Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
764ee15
REST settings services
mayankansys Jul 31, 2026
66b3c42
Added HttpSolver: Standalone REST solver independent of gRPC
mayankansys Jul 31, 2026
ae0a6c2
chore: adding changelog file 5298.added.md [dependabot-skip]
pyansys-ci-bot Jul 31, 2026
07c0da4
minor comment removal
mayankansys Jul 31, 2026
da510ea
added the default version & set the interactive mode to false
mayankansys Aug 3, 2026
27864cf
updated the attribute names & method name
mayankansys Aug 4, 2026
f84e547
updated the auth_token attribute
mayankansys Aug 5, 2026
776479f
updated the conftest.py
mayankansys Aug 6, 2026
4e3b970
modify the REST settings & client
mayankansys Aug 9, 2026
f74c540
test_settings_api file contains rest tests
mayankansys Aug 11, 2026
bec51e0
updated the tests according to the REST & gRPC (skipping tests accord…
mayankansys Aug 11, 2026
fdb4791
minor test update
mayankansys Aug 12, 2026
be43f36
changes in the flobject
mayankansys Aug 12, 2026
104c590
update the testing (factory method)
mayankansys Aug 14, 2026
f232a9f
minor change
mayankansys Aug 14, 2026
114f06b
removing the standalone_launcher.py
mayankansys Aug 14, 2026
7ab62bc
flobject minor changes
mayankansys Aug 14, 2026
4cc758d
Updated the docs strings & minor changes
mayankansys Aug 14, 2026
7db68a3
refactor: flobject & inherit the BaseSettings
mayankansys Aug 19, 2026
a496ce5
session_solver.py code moved to session_utilities.
mayankansys Aug 19, 2026
e248f68
test: conftest updated
mayankansys Aug 19, 2026
0480cb3
refactor: updated the test_settings_api.p[y file
mayankansys Aug 20, 2026
3b01d7a
updated the test_session.py file
mayankansys Aug 21, 2026
2ac969f
refactor: remove duplicate imports and fix import ordering
mayankansys Aug 31, 2026
c26c570
fix: restore Solver.from_http factory method
mayankansys Aug 31, 2026
1bb9102
refactor: remove RestSettings.create override
mayankansys Aug 31, 2026
f9ad2ef
refactor: changes according to the new structure.
mayankansys Sep 1, 2026
c573d16
Merge branch 'main' into feat/REST_settings
mayankansys Sep 1, 2026
8fcaf61
REST_settings: tests & files updated to resolve some issues
mayankansys Sep 4, 2026
32ddc52
Merge branch 'feat/REST_settings' of https://github.com/ansys/pyfluen…
mayankansys Sep 4, 2026
779e190
Merge branch 'main' into feat/REST_settings
mayankansys Sep 4, 2026
6e7f748
REST_settings : added the xfail in test_settings_api
mayankansys Sep 4, 2026
e886310
Merge branch 'feat/REST_settings' of https://github.com/ansys/pyfluen…
mayankansys Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/5298.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REST settings services
2 changes: 1 addition & 1 deletion src/ansys/fluent/core/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
Quick start::

>>> from ansys.fluent.core.rest import FluentRestClient
>>> client = FluentRestClient.connect("http://127.0.0.1:5000", auth_token="secret")
>>> client = FluentRestClient.connect("http://127.0.0.1:5000", token="secret")
>>> client.get_var("setup/models/energy/enabled")

"""
Expand Down
14 changes: 7 additions & 7 deletions src/ansys/fluent/core/rest/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

Typical use::

>>> client = FluentRestClient.connect("http://127.0.0.1:5000", auth_token="secret")
>>> client = FluentRestClient.connect("http://127.0.0.1:5000", token="secret")
>>> client.get_var("setup/models/energy/enabled")
"""

Expand Down Expand Up @@ -79,7 +79,7 @@ def __init__(
def connect(
cls,
url: str,
auth_token: str,
token: str,
*,
component: str = "fluent_1",
timeout: float = 60.0,
Expand All @@ -99,7 +99,7 @@ def connect(
url : str
Full URL of the Fluent REST server, e.g.
``"http://127.0.0.1:5000"``.
auth_token : str
token : str
Bearer token (password) set when Fluent was started.
component : str, optional
DataModel component name. Defaults to ``"fluent_1"``.
Expand All @@ -117,7 +117,7 @@ def connect(
logger.info("Connecting to Fluent REST server at %s", url)
strategy = HttpRequestStrategy(
url,
auth_token=auth_token,
token=token,
timeout=timeout,
max_retries=max_retries,
retry_delay=retry_delay,
Expand Down Expand Up @@ -228,7 +228,7 @@ def create(
FluentRestError
If the request fails.
"""
body = dict(properties) if properties else {}
body: dict[str, Any] = properties.copy() if properties else {}
if name:
body["name"] = name
return self._strategy.request("POST", f"{self._api_base}/{path}", body=body)
Expand Down Expand Up @@ -304,9 +304,9 @@ def delete_all_child_objects(self, path: str, obj_type: str) -> None:
# Commands / queries
# ------------------------------------------------------------------

def _execute(self, path: str, name: str, force: bool = False, **kwds) -> Any:
def _execute(self, path: str, command: str, force: bool = False, **kwds) -> Any:
"""POST a command/query endpoint and return response."""
endpoint = f"{self._api_base}/{path}/{urllib.parse.quote(name, safe='')}"
endpoint = f"{self._api_base}/{path}/{urllib.parse.quote(command, safe='')}"
if force:
endpoint += "?force=true"
return self._strategy.request("POST", endpoint, body=kwds)
Expand Down
29 changes: 22 additions & 7 deletions src/ansys/fluent/core/rest/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
in unit tests — no subclassing needed (structural subtyping via Protocol).
"""

import collections.abc
import hashlib
import json
import ssl
Expand All @@ -40,6 +41,20 @@
_RETRYABLE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})


def _json_default(o: Any) -> Any:
"""Fallback for ``json.dumps`` supporting non-native sequence/mapping types.

Callers may pass ``UserList``/``UserDict`` or other ``Sequence``/``Mapping``
subclasses (e.g. as command arguments) that ``json`` cannot serialize
natively; convert them to plain ``list``/``dict`` here.
"""
if isinstance(o, collections.abc.Mapping):
return dict(o)
if isinstance(o, collections.abc.Sequence) and not isinstance(o, (str, bytes)):
return list(o)
raise TypeError(f"Object of type {o.__class__.__name__} is not JSON serializable")


@runtime_checkable
class RequestStrategy(Protocol):
"""Protocol satisfied by any object that can execute an HTTP request.
Expand Down Expand Up @@ -75,11 +90,11 @@ def request(self, method: str, endpoint: str, *, body: Any = None) -> Any:
... # pragma: no cover


def _make_auth_headers(auth_token: str | None) -> dict[str, str]:
def _make_auth_headers(token: str | None) -> dict[str, str]:
"""Return ``Authorization`` header dict, or ``{}`` when there is no token."""
if not auth_token:
if not token:
return {}
token_hash = hashlib.sha256(auth_token.encode()).hexdigest()
token_hash = hashlib.sha256(token.encode()).hexdigest()
return {"Authorization": f"Bearer {token_hash}"}


Expand All @@ -91,7 +106,7 @@ class HttpRequestStrategy:
base_url : str
Root URL of the Fluent REST server, e.g. ``"http://127.0.0.1:5000"``.
A trailing slash is stripped automatically.
auth_token : str, optional
token : str, optional
Raw bearer token; SHA-256 hashed before transmission.
timeout : float, optional
Socket timeout in seconds. Defaults to ``30.0``.
Expand All @@ -107,7 +122,7 @@ def __init__(
self,
base_url: str,
*,
auth_token: str | None = None,
token: str | None = None,
timeout: float = 30.0,
max_retries: int = 2,
retry_delay: float = 1.0,
Expand All @@ -118,7 +133,7 @@ def __init__(
self._max_retries = max_retries
self._retry_delay = retry_delay
self._ssl_context = ssl_context
self._headers = _make_auth_headers(auth_token)
self._headers = _make_auth_headers(token)

# ------------------------------------------------------------------
# HTTP request execution with retry logic
Expand All @@ -136,7 +151,7 @@ def _build_request(
data: bytes | None = None
headers: dict[str, str] = dict(self._headers)
if body is not None:
data = json.dumps(body).encode("utf-8")
data = json.dumps(body, default=_json_default).encode("utf-8")
headers["Content-Type"] = "application/json"
return urllib.request.Request(
url, data=data, headers=headers, method=method.upper()
Expand Down
161 changes: 161 additions & 0 deletions src/ansys/fluent/core/services/rest_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Copyright (C) 2021 - 2026 Synopsys, Inc. and ANSYS, Inc. All rights reserved.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""REST settings service wrapper."""

from typing import Any

from ansys.fluent.core.rest.client import FluentRestClient
from ansys.fluent.core.services.settings import BaseSettings, _trace

_REST_STATIC_INFO_KEY_MAP: dict[str, str] = {
"object-type": "object_type",
"include-child-named-objects?": "include_child_named_objects",
"user-creatable?": "user_creatable",
"has-allowed-values": "has_allowed_values",
"file-purpose": "file_purpose",
"api-exposure-level": "api_exposure_level",
"deprecated-version": "deprecated_version",
"return-type": "return_type",
"child-aliases": "child_aliases",
"command-aliases": "command_aliases",
"query-aliases": "query_aliases",
"arguments-aliases": "arguments_aliases",
"allowed-values": "allowed_values",
"has-migration-adapter?": "has_migration_adapter",
}

_REST_STATIC_INFO_CONTAINER_KEYS = ("children", "commands", "queries", "arguments")


def _normalize_static_info_keys(info: dict[str, Any]) -> dict[str, Any]:
"""Recursively rename REST's hyphenated schema keys to underscore form.

Applied through ``children``/``commands``/``queries``/``arguments`` and
``object_type``. Unmapped keys are left untouched. Does not mutate *info*.
"""
if not isinstance(info, dict):
return info
normalized = {}
for key, value in info.items():
new_key = _REST_STATIC_INFO_KEY_MAP.get(key, key)
normalized[new_key] = value
for container_key in _REST_STATIC_INFO_CONTAINER_KEYS:
container = normalized.get(container_key)
if isinstance(container, dict):
normalized[container_key] = {
name: _normalize_static_info_keys(child)
for name, child in container.items()
}
object_type = normalized.get("object_type")
if isinstance(object_type, dict):
normalized["object_type"] = _normalize_static_info_keys(object_type)
return normalized


class RestSettings(BaseSettings):
"""REST-based settings service wrapper.

This class provides high-level settings operations by delegating to a
FluentRestClient instance. It is used for accessing and modifying Fluent
settings over HTTP/REST transport.

Parameters
----------
rest_client : FluentRestClient
The REST client instance to use for all settings operations.
"""

def __init__(self, rest_client: FluentRestClient) -> None:
"""Initialize the REST settings service.

Parameters
----------
rest_client : FluentRestClient
The REST client instance.
"""
super().__init__(rest_client)

@_trace
def get_static_info(self) -> dict[str, Any]:
"""Get static-info for settings.

Always requests the full schema (``full=True``); the abbreviated form
omits nested details (e.g. ``momentum``/``range`` children, a
NamedObject's ``object_type``) needed to build the settings class
tree. Keys are normalized from REST's hyphenated/``?``-suffixed form
to the underscore form ``flobject.get_cls()`` expects (matching gRPC).

Raises
------
RuntimeError
If type is empty.
"""
return _normalize_static_info_keys(self.service.get_static_info(full=True))

@_trace
def is_wildcard(self, input: str | None = None) -> bool:
"""Check whether a name contains a wildcard pattern.

``AbstractSettings`` requires this, but the REST API exposes no
equivalent of the gRPC ``Settings.IsWildcard`` endpoint, so the
fnmatch metacharacters are matched client-side instead.
"""
if input is None:
return False
return any(c in input for c in "*?[]")

@_trace
def has_wildcard(self, name: str) -> bool:
"""Check whether a name has a wildcard pattern."""
return self.is_wildcard(name)

@_trace
def execute_cmd(self, path: str, command: str, **kwds) -> Any:
"""Execute a given command with the provided keyword arguments.

The REST endpoint wraps the actual return value in an envelope of
the form ``{"result": <value>, "output": <console text>}``. Unwrap
it here so callers see the same plain value that the gRPC service
returns, instead of the raw envelope.
"""
return _unwrap_result(self.service.execute_cmd(path, command, **kwds))

@_trace
def execute_query(self, path: str, query: str, **kwds) -> Any:
"""Execute a given query with the provided keyword arguments.

See :meth:`execute_cmd` for why the response is unwrapped.
"""
return _unwrap_result(self.service.execute_query(path, query, **kwds))


def _unwrap_result(response: Any) -> Any:
"""Extract the ``"result"`` value from a command/query response envelope.

The REST API returns ``{"result": <value>, "output": <text>}`` for
command/query execution. Responses without a ``"result"`` key are
returned unchanged (defensive fallback).
"""
if isinstance(response, dict) and "result" in response:
return response["result"]
return response
Loading
Loading