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.
Files changed (152) hide show
  1. forward_sdk-0.1.1/.gitignore +14 -0
  2. forward_sdk-0.1.1/CHANGELOG.md +64 -0
  3. forward_sdk-0.1.1/LICENSE +21 -0
  4. forward_sdk-0.1.1/PKG-INFO +108 -0
  5. forward_sdk-0.1.1/README.md +77 -0
  6. forward_sdk-0.1.1/examples/README.md +20 -0
  7. forward_sdk-0.1.1/pyproject.toml +114 -0
  8. forward_sdk-0.1.1/scripts/_tidy.py +52 -0
  9. forward_sdk-0.1.1/scripts/check_generated.py +79 -0
  10. forward_sdk-0.1.1/scripts/downconvert_spec.py +332 -0
  11. forward_sdk-0.1.1/scripts/gen_models.py +105 -0
  12. forward_sdk-0.1.1/scripts/gen_operations.py +261 -0
  13. forward_sdk-0.1.1/scripts/gen_public_models.py +123 -0
  14. forward_sdk-0.1.1/scripts/gen_services.py +421 -0
  15. forward_sdk-0.1.1/scripts/sync_spec.py +174 -0
  16. forward_sdk-0.1.1/scripts/unasync.py +184 -0
  17. forward_sdk-0.1.1/spec/SPEC_SOURCE.json +8 -0
  18. forward_sdk-0.1.1/spec/coverage-allowlist.yaml +13 -0
  19. forward_sdk-0.1.1/spec/forward-openapi-3.1.json +21501 -0
  20. forward_sdk-0.1.1/spec/forward-openapi.yaml +18088 -0
  21. forward_sdk-0.1.1/spec/gating.yaml +27 -0
  22. forward_sdk-0.1.1/spec/unpublished.yaml +759 -0
  23. forward_sdk-0.1.1/src/forward_sdk/__init__.py +53 -0
  24. forward_sdk-0.1.1/src/forward_sdk/_async/__init__.py +7 -0
  25. forward_sdk-0.1.1/src/forward_sdk/_async/client.py +154 -0
  26. forward_sdk-0.1.1/src/forward_sdk/_async/services/__init__.py +0 -0
  27. forward_sdk-0.1.1/src/forward_sdk/_async/services/_base.py +51 -0
  28. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/__init__.py +158 -0
  29. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/aliases.py +77 -0
  30. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/checks.py +88 -0
  31. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/classic_devices.py +212 -0
  32. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/collection_schedules.py +99 -0
  33. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/collector_tasks.py +64 -0
  34. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/credentials.py +225 -0
  35. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/data_connectors.py +124 -0
  36. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/data_files.py +41 -0
  37. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/encryptors.py +117 -0
  38. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/endpoint_profiles.py +138 -0
  39. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/internet_node.py +131 -0
  40. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/intranet_nodes.py +191 -0
  41. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/jump_servers.py +89 -0
  42. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/l2vpns.py +181 -0
  43. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/l3vpns.py +189 -0
  44. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/legacy_collection.py +79 -0
  45. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/network_endpoints.py +255 -0
  46. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/network_locations.py +273 -0
  47. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/network_topology.py +181 -0
  48. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/path_search.py +138 -0
  49. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/system_administration.py +45 -0
  50. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/user_accounts.py +93 -0
  51. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/vulnerability_analysis.py +188 -0
  52. forward_sdk-0.1.1/src/forward_sdk/_async/services/_generated/wan_circuits.py +117 -0
  53. forward_sdk-0.1.1/src/forward_sdk/_async/services/ai.py +222 -0
  54. forward_sdk-0.1.1/src/forward_sdk/_async/services/device_tags.py +122 -0
  55. forward_sdk-0.1.1/src/forward_sdk/_async/services/devices.py +145 -0
  56. forward_sdk-0.1.1/src/forward_sdk/_async/services/networks.py +76 -0
  57. forward_sdk-0.1.1/src/forward_sdk/_async/services/nqe.py +581 -0
  58. forward_sdk-0.1.1/src/forward_sdk/_async/services/nqe_repo.py +363 -0
  59. forward_sdk-0.1.1/src/forward_sdk/_async/services/snapshots.py +410 -0
  60. forward_sdk-0.1.1/src/forward_sdk/_async/throttle.py +52 -0
  61. forward_sdk-0.1.1/src/forward_sdk/_async/transport.py +283 -0
  62. forward_sdk-0.1.1/src/forward_sdk/_generated/__init__.py +0 -0
  63. forward_sdk-0.1.1/src/forward_sdk/_generated/_base.py +58 -0
  64. forward_sdk-0.1.1/src/forward_sdk/_generated/_defs.py +74 -0
  65. forward_sdk-0.1.1/src/forward_sdk/_generated/models.py +8662 -0
  66. forward_sdk-0.1.1/src/forward_sdk/_generated/operations.py +3214 -0
  67. forward_sdk-0.1.1/src/forward_sdk/_http.py +235 -0
  68. forward_sdk-0.1.1/src/forward_sdk/_ops/__init__.py +137 -0
  69. forward_sdk-0.1.1/src/forward_sdk/_ops/_generic.py +209 -0
  70. forward_sdk-0.1.1/src/forward_sdk/_ops/ai.py +76 -0
  71. forward_sdk-0.1.1/src/forward_sdk/_ops/core.py +356 -0
  72. forward_sdk-0.1.1/src/forward_sdk/_ops/nqe.py +149 -0
  73. forward_sdk-0.1.1/src/forward_sdk/_ops/nqe_repo.py +125 -0
  74. forward_sdk-0.1.1/src/forward_sdk/_ops/rest.py +59 -0
  75. forward_sdk-0.1.1/src/forward_sdk/_sync/__init__.py +10 -0
  76. forward_sdk-0.1.1/src/forward_sdk/_sync/client.py +157 -0
  77. forward_sdk-0.1.1/src/forward_sdk/_sync/services/__init__.py +3 -0
  78. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_base.py +54 -0
  79. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/__init__.py +161 -0
  80. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/aliases.py +80 -0
  81. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/checks.py +91 -0
  82. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/classic_devices.py +215 -0
  83. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/collection_schedules.py +102 -0
  84. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/collector_tasks.py +63 -0
  85. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/credentials.py +228 -0
  86. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/data_connectors.py +127 -0
  87. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/data_files.py +44 -0
  88. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/encryptors.py +120 -0
  89. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/endpoint_profiles.py +139 -0
  90. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/internet_node.py +134 -0
  91. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/intranet_nodes.py +194 -0
  92. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/jump_servers.py +92 -0
  93. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/l2vpns.py +182 -0
  94. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/l3vpns.py +190 -0
  95. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/legacy_collection.py +80 -0
  96. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/network_endpoints.py +258 -0
  97. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/network_locations.py +274 -0
  98. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/network_topology.py +182 -0
  99. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/path_search.py +141 -0
  100. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/system_administration.py +48 -0
  101. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/user_accounts.py +96 -0
  102. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/vulnerability_analysis.py +191 -0
  103. forward_sdk-0.1.1/src/forward_sdk/_sync/services/_generated/wan_circuits.py +120 -0
  104. forward_sdk-0.1.1/src/forward_sdk/_sync/services/ai.py +222 -0
  105. forward_sdk-0.1.1/src/forward_sdk/_sync/services/device_tags.py +119 -0
  106. forward_sdk-0.1.1/src/forward_sdk/_sync/services/devices.py +146 -0
  107. forward_sdk-0.1.1/src/forward_sdk/_sync/services/networks.py +79 -0
  108. forward_sdk-0.1.1/src/forward_sdk/_sync/services/nqe.py +583 -0
  109. forward_sdk-0.1.1/src/forward_sdk/_sync/services/nqe_repo.py +362 -0
  110. forward_sdk-0.1.1/src/forward_sdk/_sync/services/snapshots.py +408 -0
  111. forward_sdk-0.1.1/src/forward_sdk/_sync/throttle.py +55 -0
  112. forward_sdk-0.1.1/src/forward_sdk/_sync/transport.py +285 -0
  113. forward_sdk-0.1.1/src/forward_sdk/_version.py +1 -0
  114. forward_sdk-0.1.1/src/forward_sdk/config.py +281 -0
  115. forward_sdk-0.1.1/src/forward_sdk/errors.py +253 -0
  116. forward_sdk-0.1.1/src/forward_sdk/models/__init__.py +770 -0
  117. forward_sdk-0.1.1/src/forward_sdk/nqe/__init__.py +21 -0
  118. forward_sdk-0.1.1/src/forward_sdk/nqe/files.py +201 -0
  119. forward_sdk-0.1.1/src/forward_sdk/nqe/pagination.py +202 -0
  120. forward_sdk-0.1.1/src/forward_sdk/nqe/query_ref.py +212 -0
  121. forward_sdk-0.1.1/src/forward_sdk/nqe/repository.py +198 -0
  122. forward_sdk-0.1.1/src/forward_sdk/nqe/telemetry.py +105 -0
  123. forward_sdk-0.1.1/src/forward_sdk/nqe/where.py +117 -0
  124. forward_sdk-0.1.1/src/forward_sdk/py.typed +0 -0
  125. forward_sdk-0.1.1/src/forward_sdk/telemetry.py +160 -0
  126. forward_sdk-0.1.1/tests/__init__.py +0 -0
  127. forward_sdk-0.1.1/tests/_async/__init__.py +0 -0
  128. forward_sdk-0.1.1/tests/_async/test_ai.py +226 -0
  129. forward_sdk-0.1.1/tests/_async/test_core_services.py +421 -0
  130. forward_sdk-0.1.1/tests/_async/test_generated_services.py +171 -0
  131. forward_sdk-0.1.1/tests/_async/test_nqe.py +605 -0
  132. forward_sdk-0.1.1/tests/_async/test_nqe_repo.py +414 -0
  133. forward_sdk-0.1.1/tests/_async/test_transport.py +470 -0
  134. forward_sdk-0.1.1/tests/_sync/__init__.py +3 -0
  135. forward_sdk-0.1.1/tests/_sync/test_ai.py +227 -0
  136. forward_sdk-0.1.1/tests/_sync/test_core_services.py +418 -0
  137. forward_sdk-0.1.1/tests/_sync/test_generated_services.py +174 -0
  138. forward_sdk-0.1.1/tests/_sync/test_nqe.py +600 -0
  139. forward_sdk-0.1.1/tests/_sync/test_nqe_repo.py +409 -0
  140. forward_sdk-0.1.1/tests/_sync/test_transport.py +467 -0
  141. forward_sdk-0.1.1/tests/conftest.py +181 -0
  142. forward_sdk-0.1.1/tests/live/__init__.py +0 -0
  143. forward_sdk-0.1.1/tests/live/test_live.py +135 -0
  144. forward_sdk-0.1.1/tests/spec/__init__.py +0 -0
  145. forward_sdk-0.1.1/tests/spec/test_conformance.py +192 -0
  146. forward_sdk-0.1.1/tests/spec/test_coverage.py +206 -0
  147. forward_sdk-0.1.1/tests/spec/test_downconvert.py +223 -0
  148. forward_sdk-0.1.1/tests/spec/test_generated_smoke.py +115 -0
  149. forward_sdk-0.1.1/tests/spec/test_unpublished_shapes.py +155 -0
  150. forward_sdk-0.1.1/tests/unit/__init__.py +0 -0
  151. forward_sdk-0.1.1/tests/unit/test_config.py +158 -0
  152. forward_sdk-0.1.1/tests/unit/test_nqe_pure.py +362 -0
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ coverage.xml
12
+ htmlcov/
13
+ site/
14
+ .env
@@ -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())