forward-sdk 0.1.1__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.
- forward_sdk-0.1.1/.gitignore +14 -0
- forward_sdk-0.1.1/CHANGELOG.md +64 -0
- forward_sdk-0.1.1/LICENSE +21 -0
- forward_sdk-0.1.1/PKG-INFO +108 -0
- forward_sdk-0.1.1/README.md +77 -0
- forward_sdk-0.1.1/examples/README.md +20 -0
- forward_sdk-0.1.1/pyproject.toml +114 -0
- forward_sdk-0.1.1/scripts/_tidy.py +52 -0
- forward_sdk-0.1.1/scripts/check_generated.py +79 -0
- forward_sdk-0.1.1/scripts/downconvert_spec.py +332 -0
- forward_sdk-0.1.1/scripts/gen_models.py +105 -0
- forward_sdk-0.1.1/scripts/gen_operations.py +261 -0
- forward_sdk-0.1.1/scripts/gen_public_models.py +123 -0
- forward_sdk-0.1.1/scripts/gen_services.py +421 -0
- forward_sdk-0.1.1/scripts/sync_spec.py +174 -0
- forward_sdk-0.1.1/scripts/unasync.py +184 -0
- forward_sdk-0.1.1/spec/SPEC_SOURCE.json +8 -0
- forward_sdk-0.1.1/spec/coverage-allowlist.yaml +13 -0
- forward_sdk-0.1.1/spec/forward-openapi-3.1.json +21501 -0
- forward_sdk-0.1.1/spec/forward-openapi.yaml +18088 -0
- forward_sdk-0.1.1/spec/gating.yaml +27 -0
- forward_sdk-0.1.1/spec/unpublished.yaml +759 -0
- forward_sdk-0.1.1/src/forward_sdk/__init__.py +53 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/__init__.py +7 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/client.py +154 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/__init__.py +0 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_base.py +51 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/__init__.py +158 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/aliases.py +77 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/checks.py +88 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/classic_devices.py +212 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/collection_schedules.py +99 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/collector_tasks.py +64 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/credentials.py +225 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/data_connectors.py +124 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/data_files.py +41 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/encryptors.py +117 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/endpoint_profiles.py +138 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/internet_node.py +131 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/intranet_nodes.py +191 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/jump_servers.py +89 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/l2vpns.py +181 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/l3vpns.py +189 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/legacy_collection.py +79 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/network_endpoints.py +255 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/network_locations.py +273 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/network_topology.py +181 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/path_search.py +138 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/system_administration.py +45 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/user_accounts.py +93 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/vulnerability_analysis.py +188 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/wan_circuits.py +117 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/ai.py +222 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/device_tags.py +122 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/devices.py +145 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/networks.py +76 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/nqe.py +581 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/nqe_repo.py +363 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/services/snapshots.py +410 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/throttle.py +52 -0
- forward_sdk-0.1.1/src/forward_sdk/_async/transport.py +283 -0
- forward_sdk-0.1.1/src/forward_sdk/_generated/__init__.py +0 -0
- forward_sdk-0.1.1/src/forward_sdk/_generated/_base.py +58 -0
- forward_sdk-0.1.1/src/forward_sdk/_generated/_defs.py +74 -0
- forward_sdk-0.1.1/src/forward_sdk/_generated/models.py +8662 -0
- forward_sdk-0.1.1/src/forward_sdk/_generated/operations.py +3214 -0
- forward_sdk-0.1.1/src/forward_sdk/_http.py +235 -0
- forward_sdk-0.1.1/src/forward_sdk/_ops/__init__.py +137 -0
- forward_sdk-0.1.1/src/forward_sdk/_ops/_generic.py +209 -0
- forward_sdk-0.1.1/src/forward_sdk/_ops/ai.py +76 -0
- forward_sdk-0.1.1/src/forward_sdk/_ops/core.py +356 -0
- forward_sdk-0.1.1/src/forward_sdk/_ops/nqe.py +149 -0
- forward_sdk-0.1.1/src/forward_sdk/_ops/nqe_repo.py +125 -0
- forward_sdk-0.1.1/src/forward_sdk/_ops/rest.py +59 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/__init__.py +10 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/client.py +157 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/__init__.py +3 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_base.py +54 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/__init__.py +161 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/aliases.py +80 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/checks.py +91 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/classic_devices.py +215 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/collection_schedules.py +102 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/collector_tasks.py +63 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/credentials.py +228 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/data_connectors.py +127 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/data_files.py +44 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/encryptors.py +120 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/endpoint_profiles.py +139 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/internet_node.py +134 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/intranet_nodes.py +194 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/jump_servers.py +92 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/l2vpns.py +182 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/l3vpns.py +190 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/legacy_collection.py +80 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/network_endpoints.py +258 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/network_locations.py +274 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/network_topology.py +182 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/path_search.py +141 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/system_administration.py +48 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/user_accounts.py +96 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/vulnerability_analysis.py +191 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/wan_circuits.py +120 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/ai.py +222 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/device_tags.py +119 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/devices.py +146 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/networks.py +79 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/nqe.py +583 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/nqe_repo.py +362 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/services/snapshots.py +408 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/throttle.py +55 -0
- forward_sdk-0.1.1/src/forward_sdk/_sync/transport.py +285 -0
- forward_sdk-0.1.1/src/forward_sdk/_version.py +1 -0
- forward_sdk-0.1.1/src/forward_sdk/config.py +281 -0
- forward_sdk-0.1.1/src/forward_sdk/errors.py +253 -0
- forward_sdk-0.1.1/src/forward_sdk/models/__init__.py +770 -0
- forward_sdk-0.1.1/src/forward_sdk/nqe/__init__.py +21 -0
- forward_sdk-0.1.1/src/forward_sdk/nqe/files.py +201 -0
- forward_sdk-0.1.1/src/forward_sdk/nqe/pagination.py +202 -0
- forward_sdk-0.1.1/src/forward_sdk/nqe/query_ref.py +212 -0
- forward_sdk-0.1.1/src/forward_sdk/nqe/repository.py +198 -0
- forward_sdk-0.1.1/src/forward_sdk/nqe/telemetry.py +105 -0
- forward_sdk-0.1.1/src/forward_sdk/nqe/where.py +117 -0
- forward_sdk-0.1.1/src/forward_sdk/py.typed +0 -0
- forward_sdk-0.1.1/src/forward_sdk/telemetry.py +160 -0
- forward_sdk-0.1.1/tests/__init__.py +0 -0
- forward_sdk-0.1.1/tests/_async/__init__.py +0 -0
- forward_sdk-0.1.1/tests/_async/test_ai.py +226 -0
- forward_sdk-0.1.1/tests/_async/test_core_services.py +421 -0
- forward_sdk-0.1.1/tests/_async/test_generated_services.py +171 -0
- forward_sdk-0.1.1/tests/_async/test_nqe.py +605 -0
- forward_sdk-0.1.1/tests/_async/test_nqe_repo.py +414 -0
- forward_sdk-0.1.1/tests/_async/test_transport.py +470 -0
- forward_sdk-0.1.1/tests/_sync/__init__.py +3 -0
- forward_sdk-0.1.1/tests/_sync/test_ai.py +227 -0
- forward_sdk-0.1.1/tests/_sync/test_core_services.py +418 -0
- forward_sdk-0.1.1/tests/_sync/test_generated_services.py +174 -0
- forward_sdk-0.1.1/tests/_sync/test_nqe.py +600 -0
- forward_sdk-0.1.1/tests/_sync/test_nqe_repo.py +409 -0
- forward_sdk-0.1.1/tests/_sync/test_transport.py +467 -0
- forward_sdk-0.1.1/tests/conftest.py +181 -0
- forward_sdk-0.1.1/tests/live/__init__.py +0 -0
- forward_sdk-0.1.1/tests/live/test_live.py +135 -0
- forward_sdk-0.1.1/tests/spec/__init__.py +0 -0
- forward_sdk-0.1.1/tests/spec/test_conformance.py +192 -0
- forward_sdk-0.1.1/tests/spec/test_coverage.py +206 -0
- forward_sdk-0.1.1/tests/spec/test_downconvert.py +223 -0
- forward_sdk-0.1.1/tests/spec/test_generated_smoke.py +115 -0
- forward_sdk-0.1.1/tests/spec/test_unpublished_shapes.py +155 -0
- forward_sdk-0.1.1/tests/unit/__init__.py +0 -0
- forward_sdk-0.1.1/tests/unit/test_config.py +158 -0
- forward_sdk-0.1.1/tests/unit/test_nqe_pure.py +362 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/) and the project uses SemVer.
|
|
5
|
+
|
|
6
|
+
## [0.1.1] - 2026-09-07
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Forward AI** (`client.ai`). Ask a question about a network in plain language
|
|
11
|
+
and get an answer grounded in one snapshot, with the tools Forward used to
|
|
12
|
+
reach it. A conversation stays pinned to its snapshot, so follow-up questions
|
|
13
|
+
are answered against the same model as the first.
|
|
14
|
+
|
|
15
|
+
This reverses an earlier decision to leave Forward AI out of scope. It is
|
|
16
|
+
unpublished and gated twice over: absent from Forward's published API, and
|
|
17
|
+
requiring the `AI_ALLOWED` organization property, so most organizations are
|
|
18
|
+
refused with a 403. That refusal arrives as an ordinary
|
|
19
|
+
`ForwardPermissionError` carrying Forward's own sentence rather than a status
|
|
20
|
+
invented here.
|
|
21
|
+
|
|
22
|
+
- `client.nqe.repo.source(path)` returns a query's committed source, fetching
|
|
23
|
+
with the flag a caller would otherwise have to remember, and failing loudly
|
|
24
|
+
when Forward returns none. Previously `RepositoryQuery.source` was simply
|
|
25
|
+
`None` when `with_source=True` had been forgotten, which reads like an empty
|
|
26
|
+
query rather than a missing argument.
|
|
27
|
+
|
|
28
|
+
## [0.1.0] - 2026-09-07
|
|
29
|
+
|
|
30
|
+
First release.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Every operation in Forward's API: 208 published, plus 11 endpoints Forward
|
|
35
|
+
does not publish that real integrations depend on. All of them described, so
|
|
36
|
+
responses are validated against a declared shape rather than read by hand.
|
|
37
|
+
- Synchronous and asynchronous clients with identical surfaces. The async one is
|
|
38
|
+
the source; the sync one is generated from it, so their retry rules, poll
|
|
39
|
+
loops and paging cannot diverge.
|
|
40
|
+
- NQE as the most developed surface: running a query inline or in the
|
|
41
|
+
background, paging or streaming results, diffing across snapshots, and
|
|
42
|
+
publishing queries to the library. Paging carries guard rails for the ways it
|
|
43
|
+
fails in production: a result set that ends before its promised total, a
|
|
44
|
+
server that stops advancing, a query larger than expected.
|
|
45
|
+
- Per-execution telemetry retained on the client, and request counters split by
|
|
46
|
+
failure cause with an observed request rate.
|
|
47
|
+
- 364 models generated from the API description, tolerant of fields and enum
|
|
48
|
+
values a newer Forward adds.
|
|
49
|
+
- Helpers for the work integrations were each doing themselves: loading `.nqe`
|
|
50
|
+
files, stripping `@primaryKey`, inlining local imports, reading a query's
|
|
51
|
+
column contract, and building escaped predicates.
|
|
52
|
+
|
|
53
|
+
### Notes
|
|
54
|
+
|
|
55
|
+
- Licensing, deployment and role-based denials arrive as the same status codes,
|
|
56
|
+
so the SDK does not claim to tell them apart. It surfaces Forward's own
|
|
57
|
+
explanation and a documented hint about what gates each group. See
|
|
58
|
+
`docs/gating.md`.
|
|
59
|
+
- NQE result rows are `dict[str, Any]`. A query's columns are defined by the
|
|
60
|
+
query, and column names are frequently not valid Python identifiers.
|
|
61
|
+
- Unpublished endpoints are isolated, described in `spec/unpublished.yaml` and
|
|
62
|
+
marked in the operation table. They are stable and used in production, but a
|
|
63
|
+
change to one would not appear in a spec diff, so this SDK's own tests are
|
|
64
|
+
what guard them. See `docs/unpublished.md`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Forward Networks, Inc.
|
|
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,108 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: forward-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for the Forward Networks REST API. A Forward Networks Field Integration.
|
|
5
|
+
Project-URL: Homepage, https://github.com/forwardnetworks/forward-python-sdk
|
|
6
|
+
Project-URL: Documentation, https://forwardnetworks.github.io/forward-python-sdk/
|
|
7
|
+
Project-URL: Repository, https://github.com/forwardnetworks/forward-python-sdk
|
|
8
|
+
Project-URL: Changelog, https://github.com/forwardnetworks/forward-python-sdk/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/forwardnetworks/forward-python-sdk/issues
|
|
10
|
+
Author-email: Forward Networks <support@forwardnetworks.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,forward,forward-networks,network,nqe,sdk
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: httpx<1,>=0.27
|
|
28
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
29
|
+
Requires-Dist: typing-extensions>=4.8; python_version < '3.11'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# forward-sdk
|
|
33
|
+
|
|
34
|
+
Python SDK for the [Forward Networks](https://www.forwardnetworks.com/) REST API.
|
|
35
|
+
|
|
36
|
+
> **A Forward Networks Field Integration.** Built and maintained by the field
|
|
37
|
+
> team to make integration work easier. It is not a supported Forward Networks
|
|
38
|
+
> product: it carries no SLA, and issues are handled on a best-effort basis
|
|
39
|
+
> rather than through Forward Support.
|
|
40
|
+
>
|
|
41
|
+
> Status: pre-release. The public API is not yet stable.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install forward-sdk
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from forward_sdk import ForwardClient
|
|
49
|
+
|
|
50
|
+
with ForwardClient.from_env() as client:
|
|
51
|
+
for network in client.networks.list():
|
|
52
|
+
print(network.id, network.name)
|
|
53
|
+
|
|
54
|
+
rows = client.nqe.query(
|
|
55
|
+
"foreach device in network.devices select {name: device.name}",
|
|
56
|
+
network_id="101",
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Authentication is HTTP basic with an API token: its access key is the username
|
|
61
|
+
and its secret is the password.
|
|
62
|
+
|
|
63
|
+
## What it covers
|
|
64
|
+
|
|
65
|
+
Every operation in Forward's API. The groups integrations use most -- NQE,
|
|
66
|
+
snapshots, networks, devices and device tags -- are hand-written with curated
|
|
67
|
+
names and behaviour such as waiting for a snapshot to finish processing. The
|
|
68
|
+
rest are generated from the OpenAPI description Forward produces from its own
|
|
69
|
+
server code.
|
|
70
|
+
|
|
71
|
+
Both a synchronous and an asynchronous client, with identical surfaces.
|
|
72
|
+
|
|
73
|
+
## Notable
|
|
74
|
+
|
|
75
|
+
**NQE first.** Running a query, paging or streaming the results, diffing a query
|
|
76
|
+
across snapshots, and publishing queries to the library are all first-class.
|
|
77
|
+
Paging carries guard rails for the ways it can go wrong in production: a result
|
|
78
|
+
set that ends early, a server that stops advancing, a query far larger than
|
|
79
|
+
expected.
|
|
80
|
+
|
|
81
|
+
**Typed, but tolerant.** Responses are pydantic models. Unknown fields and
|
|
82
|
+
unknown enum values are preserved rather than rejected, so a Forward release
|
|
83
|
+
that adds either does not break an older SDK. NQE rows stay `dict[str, Any]`,
|
|
84
|
+
because a query's columns are defined by the query.
|
|
85
|
+
|
|
86
|
+
**Honest about refusals.** A missing licence, a feature absent from your
|
|
87
|
+
deployment, and a role-based denial all arrive as the same status codes, so the
|
|
88
|
+
SDK does not pretend to tell them apart. It gives you Forward's own explanation
|
|
89
|
+
and a documented hint about what gates that operation.
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
<https://forwardnetworks.github.io/forward-python-sdk/>
|
|
94
|
+
|
|
95
|
+
- [Quickstart](docs/quickstart.md)
|
|
96
|
+
- [Configuration](docs/configuration.md)
|
|
97
|
+
- [NQE guide](docs/nqe/index.md)
|
|
98
|
+
- [Availability and gating](docs/gating.md)
|
|
99
|
+
- [Migrating from the NetBox plugin](docs/migration/netbox.md) or
|
|
100
|
+
[the Nautobot plugin](docs/migration/nautobot.md)
|
|
101
|
+
|
|
102
|
+
## Requirements
|
|
103
|
+
|
|
104
|
+
Python 3.10 or newer.
|
|
105
|
+
|
|
106
|
+
## Licence
|
|
107
|
+
|
|
108
|
+
MIT.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# forward-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Forward Networks](https://www.forwardnetworks.com/) REST API.
|
|
4
|
+
|
|
5
|
+
> **A Forward Networks Field Integration.** Built and maintained by the field
|
|
6
|
+
> team to make integration work easier. It is not a supported Forward Networks
|
|
7
|
+
> product: it carries no SLA, and issues are handled on a best-effort basis
|
|
8
|
+
> rather than through Forward Support.
|
|
9
|
+
>
|
|
10
|
+
> Status: pre-release. The public API is not yet stable.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install forward-sdk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from forward_sdk import ForwardClient
|
|
18
|
+
|
|
19
|
+
with ForwardClient.from_env() as client:
|
|
20
|
+
for network in client.networks.list():
|
|
21
|
+
print(network.id, network.name)
|
|
22
|
+
|
|
23
|
+
rows = client.nqe.query(
|
|
24
|
+
"foreach device in network.devices select {name: device.name}",
|
|
25
|
+
network_id="101",
|
|
26
|
+
)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Authentication is HTTP basic with an API token: its access key is the username
|
|
30
|
+
and its secret is the password.
|
|
31
|
+
|
|
32
|
+
## What it covers
|
|
33
|
+
|
|
34
|
+
Every operation in Forward's API. The groups integrations use most -- NQE,
|
|
35
|
+
snapshots, networks, devices and device tags -- are hand-written with curated
|
|
36
|
+
names and behaviour such as waiting for a snapshot to finish processing. The
|
|
37
|
+
rest are generated from the OpenAPI description Forward produces from its own
|
|
38
|
+
server code.
|
|
39
|
+
|
|
40
|
+
Both a synchronous and an asynchronous client, with identical surfaces.
|
|
41
|
+
|
|
42
|
+
## Notable
|
|
43
|
+
|
|
44
|
+
**NQE first.** Running a query, paging or streaming the results, diffing a query
|
|
45
|
+
across snapshots, and publishing queries to the library are all first-class.
|
|
46
|
+
Paging carries guard rails for the ways it can go wrong in production: a result
|
|
47
|
+
set that ends early, a server that stops advancing, a query far larger than
|
|
48
|
+
expected.
|
|
49
|
+
|
|
50
|
+
**Typed, but tolerant.** Responses are pydantic models. Unknown fields and
|
|
51
|
+
unknown enum values are preserved rather than rejected, so a Forward release
|
|
52
|
+
that adds either does not break an older SDK. NQE rows stay `dict[str, Any]`,
|
|
53
|
+
because a query's columns are defined by the query.
|
|
54
|
+
|
|
55
|
+
**Honest about refusals.** A missing licence, a feature absent from your
|
|
56
|
+
deployment, and a role-based denial all arrive as the same status codes, so the
|
|
57
|
+
SDK does not pretend to tell them apart. It gives you Forward's own explanation
|
|
58
|
+
and a documented hint about what gates that operation.
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
<https://forwardnetworks.github.io/forward-python-sdk/>
|
|
63
|
+
|
|
64
|
+
- [Quickstart](docs/quickstart.md)
|
|
65
|
+
- [Configuration](docs/configuration.md)
|
|
66
|
+
- [NQE guide](docs/nqe/index.md)
|
|
67
|
+
- [Availability and gating](docs/gating.md)
|
|
68
|
+
- [Migrating from the NetBox plugin](docs/migration/netbox.md) or
|
|
69
|
+
[the Nautobot plugin](docs/migration/nautobot.md)
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
Python 3.10 or newer.
|
|
74
|
+
|
|
75
|
+
## Licence
|
|
76
|
+
|
|
77
|
+
MIT.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Runnable scripts. Each reads its connection settings from the environment:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
export FORWARD_URL=https://fwd.app
|
|
7
|
+
export FORWARD_USERNAME=your-token-access-key
|
|
8
|
+
export FORWARD_PASSWORD=your-token-secret
|
|
9
|
+
export FORWARD_NETWORK_ID=101
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| Script | Shows |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `list_networks.py` | Connecting, and listing networks and snapshots |
|
|
15
|
+
| `run_query.py` | Running an NQE query and reading rows |
|
|
16
|
+
| `stream_large_query.py` | Streaming a large result set without buffering it |
|
|
17
|
+
| `device_inventory.py` | Paging through devices and writing a CSV |
|
|
18
|
+
| `publish_queries.py` | Publishing `.nqe` files to the query library |
|
|
19
|
+
| `ask_forward_ai.py` | Asking Forward AI a question, and handling not having it |
|
|
20
|
+
| `async_client.py` | The same work with the asynchronous client |
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "forward-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python SDK for the Forward Networks REST API. A Forward Networks Field Integration."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Forward Networks", email = "support@forwardnetworks.com" }]
|
|
14
|
+
keywords = ["forward", "forward-networks", "network", "nqe", "sdk", "api"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: System :: Networking",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"httpx>=0.27,<1",
|
|
31
|
+
"pydantic>=2.6,<3",
|
|
32
|
+
"typing_extensions>=4.8; python_version < '3.11'",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/forwardnetworks/forward-python-sdk"
|
|
37
|
+
Documentation = "https://forwardnetworks.github.io/forward-python-sdk/"
|
|
38
|
+
Repository = "https://github.com/forwardnetworks/forward-python-sdk"
|
|
39
|
+
Changelog = "https://github.com/forwardnetworks/forward-python-sdk/blob/main/CHANGELOG.md"
|
|
40
|
+
Issues = "https://github.com/forwardnetworks/forward-python-sdk/issues"
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=8.3",
|
|
45
|
+
"pytest-cov>=5",
|
|
46
|
+
"anyio>=4",
|
|
47
|
+
"ruff>=0.9",
|
|
48
|
+
"mypy>=1.13",
|
|
49
|
+
"pyyaml>=6",
|
|
50
|
+
"types-PyYAML",
|
|
51
|
+
"openapi-core>=0.19",
|
|
52
|
+
# Upper bound tracks what openapi-core allows; without it a dependency update
|
|
53
|
+
# proposes a version the two cannot satisfy together.
|
|
54
|
+
"openapi-spec-validator>=0.7,<0.9",
|
|
55
|
+
"pre-commit>=4",
|
|
56
|
+
]
|
|
57
|
+
codegen = [
|
|
58
|
+
"datamodel-code-generator[http]>=0.28",
|
|
59
|
+
"pyyaml>=6",
|
|
60
|
+
]
|
|
61
|
+
docs = [
|
|
62
|
+
"mkdocs>=1.6",
|
|
63
|
+
"mkdocs-material>=9.5",
|
|
64
|
+
"mkdocstrings[python]>=0.27",
|
|
65
|
+
"pymdown-extensions>=10",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.hatch.version]
|
|
69
|
+
path = "src/forward_sdk/_version.py"
|
|
70
|
+
|
|
71
|
+
[tool.hatch.build.targets.sdist]
|
|
72
|
+
include = ["src/forward_sdk", "spec", "scripts", "tests", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
73
|
+
|
|
74
|
+
[tool.hatch.build.targets.wheel]
|
|
75
|
+
packages = ["src/forward_sdk"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
line-length = 100
|
|
79
|
+
target-version = "py310"
|
|
80
|
+
src = ["src", "tests", "scripts"]
|
|
81
|
+
extend-exclude = ["src/forward_sdk/_generated", "src/forward_sdk/_sync", "tests/_sync"]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint]
|
|
84
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "PL"]
|
|
85
|
+
ignore = ["PLR0913", "PLR2004", "PLR0912", "PLR0915", "PLR0911", "RUF012"]
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint.per-file-ignores]
|
|
88
|
+
"tests/**" = ["PLR", "B011"]
|
|
89
|
+
"scripts/**" = ["PLR"]
|
|
90
|
+
|
|
91
|
+
[tool.mypy]
|
|
92
|
+
python_version = "3.10"
|
|
93
|
+
strict = true
|
|
94
|
+
warn_unreachable = true
|
|
95
|
+
plugins = ["pydantic.mypy"]
|
|
96
|
+
files = ["src", "tests", "scripts", "examples"]
|
|
97
|
+
exclude = ["src/forward_sdk/_generated/models.py"]
|
|
98
|
+
|
|
99
|
+
[[tool.mypy.overrides]]
|
|
100
|
+
module = ["forward_sdk._generated.models"]
|
|
101
|
+
ignore_errors = true
|
|
102
|
+
|
|
103
|
+
[tool.pytest.ini_options]
|
|
104
|
+
testpaths = ["tests"]
|
|
105
|
+
markers = [
|
|
106
|
+
"live: tests that hit a real Forward instance (need FORWARD_URL/USERNAME/PASSWORD/NETWORK_ID)",
|
|
107
|
+
]
|
|
108
|
+
addopts = "-m 'not live' -ra"
|
|
109
|
+
pythonpath = ["."]
|
|
110
|
+
filterwarnings = ["error::DeprecationWarning:forward_sdk"]
|
|
111
|
+
|
|
112
|
+
[tool.coverage.run]
|
|
113
|
+
source = ["forward_sdk"]
|
|
114
|
+
omit = ["src/forward_sdk/_generated/*"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Format generated Python before it is written.
|
|
2
|
+
|
|
3
|
+
Generators emit correct but unpolished code: imports that a particular module
|
|
4
|
+
turns out not to need, lines past the limit. Formatting the text *before*
|
|
5
|
+
comparing it with what is on disk keeps generation idempotent, which the CI
|
|
6
|
+
freshness check depends on.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
|
|
14
|
+
__all__ = ["tidy"]
|
|
15
|
+
|
|
16
|
+
# Import sorting, unused-import removal, and __all__ sorting. Letting the linter
|
|
17
|
+
# apply its own ordering avoids generators guessing at it and drifting.
|
|
18
|
+
FIX_RULES = "I,F401,RUF022"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def tidy(source: str, filename: str) -> str:
|
|
22
|
+
"""Return ``source`` with imports sorted and the file formatted."""
|
|
23
|
+
if shutil.which("ruff") is None:
|
|
24
|
+
return source
|
|
25
|
+
|
|
26
|
+
fixed = subprocess.run(
|
|
27
|
+
[
|
|
28
|
+
"ruff",
|
|
29
|
+
"check",
|
|
30
|
+
"--fix-only",
|
|
31
|
+
"--quiet",
|
|
32
|
+
"--select",
|
|
33
|
+
FIX_RULES,
|
|
34
|
+
"--stdin-filename",
|
|
35
|
+
filename,
|
|
36
|
+
"-",
|
|
37
|
+
],
|
|
38
|
+
input=source,
|
|
39
|
+
capture_output=True,
|
|
40
|
+
text=True,
|
|
41
|
+
check=False,
|
|
42
|
+
)
|
|
43
|
+
body = fixed.stdout if fixed.returncode == 0 and fixed.stdout else source
|
|
44
|
+
|
|
45
|
+
formatted = subprocess.run(
|
|
46
|
+
["ruff", "format", "--quiet", "--stdin-filename", filename, "-"],
|
|
47
|
+
input=body,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
check=False,
|
|
51
|
+
)
|
|
52
|
+
return formatted.stdout if formatted.returncode == 0 and formatted.stdout else body
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Fail if checked-in generated code is stale.
|
|
2
|
+
|
|
3
|
+
Regenerates everything derived from the vendored spec and reports any file that
|
|
4
|
+
changed. CI runs this so a hand-edit of generated code, or a spec sync whose
|
|
5
|
+
generators were never re-run, cannot land.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
GENERATED = (
|
|
16
|
+
Path("spec/forward-openapi-3.1.json"),
|
|
17
|
+
Path("src/forward_sdk/_generated/models.py"),
|
|
18
|
+
Path("src/forward_sdk/_generated/operations.py"),
|
|
19
|
+
Path("src/forward_sdk/models/__init__.py"),
|
|
20
|
+
Path("src/forward_sdk/_async/services/_generated"),
|
|
21
|
+
Path("src/forward_sdk/_sync"),
|
|
22
|
+
Path("tests/_sync"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Ordered: services are generated from the operation table, and the sync tree
|
|
26
|
+
# from everything above it.
|
|
27
|
+
STEPS = (
|
|
28
|
+
("down-convert", ["scripts/downconvert_spec.py"]),
|
|
29
|
+
("models", ["scripts/gen_models.py"]),
|
|
30
|
+
("operations", ["scripts/gen_operations.py"]),
|
|
31
|
+
("services", ["scripts/gen_services.py"]),
|
|
32
|
+
("public models", ["scripts/gen_public_models.py"]),
|
|
33
|
+
("unasync", ["scripts/unasync.py"]),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv: list[str] | None = None) -> int:
|
|
38
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--fast",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="only run the unasync step (used as a pre-commit hook)",
|
|
43
|
+
)
|
|
44
|
+
args = parser.parse_args(argv)
|
|
45
|
+
|
|
46
|
+
steps = [s for s in STEPS if not args.fast or s[0] == "unasync"]
|
|
47
|
+
for name, argv_step in steps:
|
|
48
|
+
script = Path(argv_step[0])
|
|
49
|
+
if not script.exists():
|
|
50
|
+
print(f"skipping {name}: {script} does not exist yet")
|
|
51
|
+
continue
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
[sys.executable, *argv_step], capture_output=True, text=True, check=False
|
|
54
|
+
)
|
|
55
|
+
if result.returncode != 0:
|
|
56
|
+
print(result.stdout)
|
|
57
|
+
print(result.stderr, file=sys.stderr)
|
|
58
|
+
raise SystemExit(f"generator {name} failed")
|
|
59
|
+
|
|
60
|
+
existing = [str(p) for p in GENERATED if p.exists()]
|
|
61
|
+
# `git status --porcelain` rather than `git diff`: a newly generated file is
|
|
62
|
+
# untracked, and a diff would not mention it at all.
|
|
63
|
+
status = subprocess.run(
|
|
64
|
+
["git", "status", "--porcelain", "--", *existing],
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
check=False,
|
|
68
|
+
)
|
|
69
|
+
if status.stdout.strip():
|
|
70
|
+
print("Generated code is stale. Re-run the generators and commit the result:\n")
|
|
71
|
+
print(status.stdout)
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
print("generated code is up to date")
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
sys.exit(main())
|