feat: rest layered#5220
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors the ansys.fluent.core.rest client into a layered architecture by separating REST error modeling, HTTP transport mechanics, and the Fluent settings API surface, enabling unit-testing of the API layer via an injectable transport strategy.
Changes:
- Introduces
errors.py(FluentRestError) andtransport.py(RequestStrategy+HttpRequestStrategy) and rewritesclient.pyto be transport-agnostic via strategy injection. - Updates the package surface in
rest/__init__.py(re-exports +connect_to_webserveralias) and adds areal_serverpytest marker + real-server fixture scaffolding. - Adds a large REST test suite and integration-test fixtures (but the new tests currently target the pre-refactor API and need updating to the new layering).
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_rest.py | Adds REST tests, but currently written against the old FluentRestClient(url, auth_token, ...) API and removed lifecycle/rest_connect module. |
| tests/conftest.py | Adds real-server probing and a real_client fixture for integration tests (needs to use FluentRestClient.connect() under the new design). |
| src/ansys/fluent/core/rest/transport.py | New transport layer with retry/backoff, auth header hashing, and RequestStrategy Protocol. |
| src/ansys/fluent/core/rest/errors.py | New centralized REST error type and transport exception translation. |
| src/ansys/fluent/core/rest/client.py | Rewritten API client using injected strategy; needs to align 404 behavior with documented contract for list/name helpers. |
| src/ansys/fluent/core/rest/init.py | Updates public exports and provides connect_to_webserver alias to FluentRestClient.connect. |
| pyproject.toml | Adds real_server marker. |
| doc/changelog.d/5220.added.md | Adds changelog fragment for the REST layering feature. |
| doc/changelog.d/5015.added.md | Adds changelog fragment related to REST connection. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…rt patches - Add FakeStrategy test double implementing RequestStrategy.request(); records all (method, endpoint, body) calls in a queue-based FIFO. - Fix _client() helper to build FluentRestClient(FakeStrategy()) instead of the removed FluentRestClient(base_url, auth_token=...) API. - Add _http_strategy() helper for HttpRequestStrategy-level tests. - Rename TestFluentRestClientInit → TestHttpRequestStrategyInit; assertions now target HttpRequestStrategy attributes (_base_url, _headers, _timeout, etc.) and FluentRestClient._api_base. - Split TestFluentRestClientAuth into TestAuthHelpers (testing the _make_auth_headers helper in transport.py) and TestHttpRequestStrategyAuth (patching transport.urllib, not client.urllib). - Rewrite Reads/Writes/NamedObjects/Execute test classes to check strategy.calls instead of inspecting urllib Request objects. - Remove TestFluentRestClientLifecycle (exit/_is_closed/context-manager functionality was removed from FluentRestClient in the refactor). - Rename TestFluentRestClientTransport → TestHttpRequestStrategyTransport; all patches now target ansys.fluent.core.rest.transport.urllib.request.urlopen. - Rewrite TestConnectToWebserver to inspect client._strategy attributes. - Fix test_all_exports to include HttpRequestStrategy and RequestStrategy. - Fix classmethod identity checks to use __func__ comparison.
|
@Gobot1234 Thanks for the comments. This PR is a refactored version of the current PR for the same version. Not all details are finalised. Tests have not been updated and testing guidance is given in the description. @mayankansys Would you like to either take over this PR or incorporate the concepts into your PR? @Gobot1234's need to be looked at. |
REST Client Refactoring: Layered Architecture with Injectable Transport Strategy
Summary
The original
rest/client.pywas a single 415-line file that handled fourdistinct responsibilities at once: error modelling, HTTP transport mechanics,
authentication, and the Fluent DataModel settings API. This PR splits those
responsibilities across three focused modules and introduces a strategy
interface that makes the API layer independently unit-testable without a
running Fluent server and without patching Python's standard library.
Table of Contents
1. The Problem: One File Doing Too Much
The original
client.pycontained four interleaved layers of code:Every one of those items lived in a single class.
2. Why That Matters
It is hard to test
To test whether
delete(..., ignore_not_found=True)correctly swallows a 404but still re-raises a 500, you would previously have needed to either:
urllib.request.urlopenat a very low level — a fragileapproach that breaks if the internal implementation changes.
The API logic (what the method does with the response) and the transport
mechanics (how the HTTP call is made) were fused together, so you could not
test one without the other.
It is hard to reason about
When reading the retry logic, you have to hold in your head that it sits
inside the same class as
get_varandcreate. When readingget_var,you have to remember that
self._requestquietly does retries, checks aclosed-session flag, builds the URL, and SHA-256-hashes your token before
touching the network. That mental overhead accumulates.
The closed-session guard was misplaced
The caller of
_request(i.e., every API method) had to silently rely on thefact that an internal flag was checked. The error constructor arguments
(
0,"Session is closed") were copy-pasted knowledge rather than a namedconcept.
Error construction was fragile
The numeric status code
0as a sentinel for "no network connection" was animplicit convention, not an enforced one.
3. The New Module Structure
rest_connect.pyhas been deleted — its single function is now theFluentRestClient.connect()classmethod (see §4.5).Line counts after refactoring:
errors.pytransport.pyclient.py__init__.py4. Design Decisions — Detailed Rationale
4.1
errors.py— one home for all error knowledgeBefore:
After:
Two things changed:
_RETRYABLE_STATUS_CODESmoved fromclient.pytoerrors.py. It wasalways used only by
FluentRestError.from_transport— placing it next tothat method means the two pieces of related knowledge live together.
A future maintainer changing which codes are retryable will find the
constant in the file dedicated to error types, not buried in the API client.
Named factory methods replace raw constructor calls. Instead of every
call site knowing that
status=0means "no connection" and typing out theexact string
"Session is closed", each distinct error shape has one namedconstructor method. This is not merely stylistic: it means the error's
meaning is encoded in the code, not in a magic number and an English string
that could silently diverge.
4.2
transport.py— isolating HTTP mechanicsBefore: the HTTP mechanics were methods on
FluentRestClient, interspersedwith the Fluent API methods. To follow the retry logic you had to navigate
between
_request,_send_with_retry,_back_off,_send,_send_once,and
_build_requestwhile mentally filtering out the unrelatedget_varanddeletemethods around them.After:
HttpRequestStrategycontains all five HTTP-level steps and nothingelse:
All the constructor arguments that belonged to the transport (url, auth_token,
timeout, max_retries, retry_delay, ssl_context) now live on
HttpRequestStrategy.__init__, not onFluentRestClient.__init__.FluentRestClientno longer knows that HTTPS exists._make_auth_headers— previously a@staticmethodonFluentRestClient—is now a module-level private function in
transport.py. It was always onlycalled once, from
HttpRequestStrategy.__init__, so tying it to the APIclient class was misleading.
4.3 The
RequestStrategyProtocol — the key to testabilityThis is the most important change in the PR.
A Protocol in Python is a way of saying: "anything that has these
methods, in this shape, counts as satisfying this contract." Unlike a
traditional base class, you do not have to explicitly inherit from the Protocol.
If your object has the right method signatures, it qualifies automatically.
This is called structural subtyping or duck typing with a formal contract.
FluentRestClientnow declares that it needs anyRequestStrategy:In production you pass an
HttpRequestStrategy:In tests you pass anything with a matching
requestmethod:The
FakeStrategydoes not importurllib, does not touch the network,does not need a Fluent license, and takes zero milliseconds to "respond."
Yet because
FluentRestClientonly ever callsself._strategy.request(...),the API layer runs exactly as it would in production.
The
@runtime_checkabledecorator means you can useisinstance()in teststo assert that your fake satisfies the contract:
4.4
client.py— pure API, no HTTPFluentRestClientis now entirely free of HTTP knowledge. Every methodsimply constructs the correct endpoint string for that operation and delegates
to
self._strategy.request(...).Before (
_requestcalled directly):After (strategy called directly):
The visible difference is small, but the consequence is large:
_requestwas an internal method that mingled transport concerns;
self._strategyis aninjected object with a documented interface. Any reader who wonders "what
does
requestactually do?" can find the answer intransport.py— or, whenwriting a test, can supply their own answer.
4.5
FluentRestClient.connect()— replacingrest_connect.pyrest_connect.pycontained one function:After the refactor, constructing a production client requires assembling two
objects (
HttpRequestStrategy+FluentRestClient). Placing that assemblylogic in a classmethod keeps it discoverable and co-located with the class it
produces:
rest_connect.pyis deleted. For backwards compatibility,connect_to_webserveris re-exported from__init__.pyas a plain alias:Existing callers of
connect_to_webserver(url, auth_token)continue to workwithout any changes.
4.6
_names_from/_size_from— promoted to module-level functionsBefore: private
@staticmethods onFluentRestClient.After: private module-level functions in
client.py.These are pure functions — they take a value, return a value, and have no
side effects or dependencies. Attaching them to the class implied they needed
access to
self, which they never did. As module-level functions they can beimported and tested in complete isolation, without instantiating any class at
all.
4.7 Session lifecycle — removed
The original
FluentRestClientincluded:These were removed for two reasons:
exit()is just an API call. SendingPOST /api/app/exitis nodifferent in kind from calling
execute_cmd("app", "exit"). Elevating itto a special lifecycle method with state management (
_is_closed) implieda privileged status it does not actually have.
_is_closedleaked session-management concerns into the transport.The closed-session guard (
if self._is_closed: raise FluentRestError(0, "Session is closed")) was checked in_request, meaning the transportlayer was aware of application-level session state. These are different
layers. The caller — not the client — is responsible for knowing when to
stop making requests.
5. File-by-File Breakdown
rest/errors.py(new file)_RETRYABLE_STATUS_CODESclient.py; belongs with its sole consumerfrom_transportFluentRestError.__init__FluentRestError.from_transport()client.py; unchangedFluentRestError.not_found()rest/transport.py(new file)_RETRYABLE_METHODSclient.pyRequestStrategy@runtime_checkable Protocol_make_auth_headers()FluentRestClient._make_auth_headersstaticmethodHttpRequestStrategyFluentRestClient; owns all 6 HTTP methodsrest/client.py(rewritten)FluentRestClient.__init__strategy: RequestStrategyonlyFluentRestClient.connect()rest_connect.pyself._request(...)→self._strategy.request(...)_names_from()/_size_from()@staticmethodto module-level functionsexit,__enter__,__exit__,_is_closed_make_auth_headers,_build_request,_send_once,_send,_back_off,_send_with_retry,_requesttransport.py)rest/__init__.py(updated)FluentRestClientFluentRestErrorerrors.pydirectlyHttpRequestStrategyRequestStrategyconnect_to_webserverFluentRestClient.connect; backwards compatiblerest/rest_connect.py(deleted)The module's single function is superseded by
FluentRestClient.connect().6. How to Write Unit Tests
The refactoring was motivated specifically by the desire to make the API layer
unit-testable. This section describes the concrete testing patterns it
enables.
6.1 The core idea:
FakeStrategyA fake (sometimes called a test double or stub) is a minimal
replacement object used in tests. It behaves enough like the real thing to
let the code under test run, but it does not do any real work — in this case,
it never opens a network connection.
The simplest possible fake is just a class with one method:
That is all
FluentRestClientever asks of its strategy. The protocol checkcan be verified:
In practice, fakes are more interesting — they record calls or return
pre-programmed responses:
This fake lets tests assert what was sent to the server as well as
controlling what the server returns.
6.2 Testing individual API methods
get_vardeletewithignore_not_foundget_attrs— URL encodingdelete_all_child_objects— composition of callsexecute_cmd—?force=trueflagComponent name is honoured
6.3 Testing error handling in the API layer
6.4 Testing the transport layer in isolation
HttpRequestStrategycan be tested independently ofFluentRestClient,focusing purely on retry logic and authentication:
6.5 Testing
FluentRestErrorfactories6.6 Testing
_names_fromand_size_fromBecause these are now module-level pure functions, they are trivially
importable and testable:
These tests previously required instantiating
FluentRestClientto reach the@staticmethods.7. Backwards Compatibility
All existing callers continue to work without any modification:
from ansys.fluent.core.rest import FluentRestClientfrom ansys.fluent.core.rest import FluentRestErrorfrom ansys.fluent.core.rest import connect_to_webserverfrom ansys.fluent.core.rest.client import FluentRestClientfrom ansys.fluent.core.rest.client import FluentRestErrorerrors.pyfrom ansys.fluent.core.rest.rest_connect import connect_to_webserverThe last two rows are internal import paths not present in the public API.
Any code that imported
FluentRestErrordirectly fromclient(rather thanfrom the package root) will need to update its import to:
8. Dependency Graph
The dependency graph is acyclic and flows strictly in one direction:
errors←transport←client←__init__.No layer imports from a layer above it.
errors.pyhas no internaldependencies at all — it only uses the Python standard library. This means
any of the three inner modules can be imported, read, and tested without
pulling in the others.