Compare commits

..

21 Commits

Author SHA1 Message Date
IluaAir
0b92e342e5 Enhance error handling in OxiAPI and Node classes
- Updated the `reload` method in the `OxiAPI` class to catch `HTTPError` exceptions and raise a custom `OxiAPIError` with context.
- Improved the `__call__` method in the `Node` class to handle `HTTPError` exceptions similarly, providing context-specific error messages.
- Introduced a new class method `from_http_error` in `OxiAPIError` for standardized error message generation based on HTTP status codes.
2026-03-26 20:10:05 +03:00
IluaAir
1cc225917e Add OxiApi create_session method for better view 2026-03-26 19:51:51 +03:00
IluaAir
61892d8f51 Add OxiAPIError exception class for improved error handling
- Introduced a new `OxiAPIError` class to standardize error reporting in the OxiAPI.
- The class includes an optional status code for enhanced context in error messages.
2026-03-26 00:39:43 +03:00
IluaAir
8cebbf743a Add OxiAdapter for enhanced HTTP request handling in OxiAPI
- Introduced a new `OxiAdapter` class that extends `HTTPAdapter` to manage timeouts and retries for HTTP requests.
- Integrated the `OxiAdapter` into the `OxiAPI` class, setting a default timeout and enabling retry logic for both HTTP and HTTPS requests.
2026-03-26 00:31:13 +03:00
IluaAir
a107662e99 Enhance OxiAPI and Node classes with type hints and property updates
- Updated the `OxiAPI` class to check for `None` explicitly when setting authentication credentials.
- Added type hints to the `Node` class and introduced a TODO for future enhancements.
- Refactored properties in the `NodeView` class to include type hints and improved handling of optional data retrieval.
2026-03-18 00:17:14 +03:00
IluaAir
1d0f5ed685 Refactor Quasar model by removing system method and updating TTP template
- Removed the `system` method from the `Quasar` model to streamline system information handling.
- Updated the TTP template to enhance the formatting of system details, including version and product information, for improved clarity and organization.
2026-03-18 00:01:22 +03:00
IluaAir
5b8380aeee Add system method to Quasar model and update TTP template
- Implemented a new `system` method in the `Quasar` model to extract and format system information, including version handling.
- Updated the TTP template to adjust the grouping and ignore patterns for better parsing of system details, ensuring compatibility with the new method.
2026-03-17 01:07:23 +03:00
IluaAir
65c82fbaf5 Refactor error handling in Node class
- Updated the `Node` class to use `response.raise_for_status()` for improved error handling, replacing the previous manual check for a 500 status code. This change simplifies the error management process when fetching node data.
2026-03-16 18:24:24 +03:00
IluaAir
974fff6038 Update TTP template to escape whitespace in ignore patterns
- Modified the TTP template to use double backslashes for escaping whitespace in the `ignore` function for both `interfaces` and `bulkinterfaces` groups, ensuring proper parsing of configuration lines.
2026-03-13 13:12:02 +03:00
IluaAir
586e52282b Refactor Quasar interface handling and update TTP template
- Enhanced the `interfaces` method in the `Quasar` model to process bulk interfaces, returning a structured list of interface details.
- Updated the TTP template to reflect the change from `interfaces` to `bulkinterfaces` for better organization of interface configurations.
2026-03-12 23:39:47 +03:00
IluaAir
e3392f6c76 Add reload method to OxiAPI class
- Implemented a new `reload` method to fetch the reload status from the API.
- The method raises an error for unsuccessful responses and returns the status code on success.
2026-03-12 20:16:33 +03:00
IluaAir
de0e09af9d Add node refresh functionality to NodeView
- Implemented a private `_updater` method to fetch the next node's status.
- Added `last_status` and `last_check` properties to retrieve the latest node status and check time.
- Introduced a `refresh` property to update the node status and handle errors appropriately.
2026-03-12 20:13:02 +03:00
IluaAir
ca96d2600a Add Quasar model and TTP template
- Introduced a new `Quasar` model for parsing Quasar devices.
- Created a corresponding TTP template defining required and optional groups for configuration parsing.
2026-03-11 23:29:08 +03:00
IluaAir
56eae15e27 Update README.md to include new device models
- Added Eltex, H3C, and Quasar to the list of supported device models with their corresponding identifiers.
2026-03-11 23:26:35 +03:00
IluaAir
db79199319 Add LICENSE file and update pyproject.toml
- Added Apache License 2.0 to the project.
- Updated project description in pyproject.toml to "Oxidized API client".
- Specified the LICENSE file in pyproject.toml.
- Added classifiers for Python version and license type.
2026-03-11 23:23:23 +03:00
i.shramko
2e109db121 Update Qtech:
- Add full support of diff types of switches
- Fix default system parser
2026-03-10 18:41:54 +03:00
i.shramko
b9dce8e417 Update config:
- add by_alias attr
Update contract:
- del aliase for Vlans vlan_id
Update qtech.ttp:
- fix vla_id _start_ method
2026-03-10 17:53:51 +03:00
i.shramko
d185dc6c7c Update config:
- add dump() for dict| list overview
- del qtech.ttp _start_ method
2026-03-10 16:56:13 +03:00
i.shramko
68566a24fb Update qtech.ttp:
- add interface name ORPHRASE
2026-03-10 16:00:57 +03:00
i.shramko
08733bd493 Merge remote-tracking branch 'origin/dev' into dev 2026-03-10 15:52:56 +03:00
i.shramko
a1c57733f6 Update pyproject.toml:
- Add package-data with resource .ttp
2026-03-10 15:52:38 +03:00
14 changed files with 471 additions and 27 deletions

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -310,6 +310,9 @@ print(interfaces.json())
| MikroTik | `routeros`, `ros`, `mikrotik` | | MikroTik | `routeros`, `ros`, `mikrotik` |
| Qtech | `qtech` | | Qtech | `qtech` |
| Huawei | `huawei`, `vrp` | | Huawei | `huawei`, `vrp` |
| Eltex | `eltex` |
| H3C | `h3c` |
| Quasar | `qos`, `quasar` |
Ключи реестра — это значения поля `model`, возвращаемого API для узла. Регистр не учитывается. Ключи реестра — это значения поля `model`, возвращаемого API для узла. Регистр не учитывается.

21
oxi/adapter.py Normal file
View File

@@ -0,0 +1,21 @@
from typing import Optional
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
class OxiAdapter(HTTPAdapter):
def __init__(
self,
timeout: Optional[int] = None,
max_retries: int = 3,
*args,
**kwargs,
):
self.timeout = timeout
retry = Retry(total=max_retries, backoff_factor=0.3)
super().__init__(*args, max_retries=retry, **kwargs)
def send(self, request, **kwargs):
if kwargs.get("timeout") is None:
kwargs["timeout"] = self.timeout
return super().send(request, **kwargs)

View File

@@ -16,7 +16,7 @@ class ModelView(Generic[TModel]):
def __init__(self, model: TModel | list[TModel]): def __init__(self, model: TModel | list[TModel]):
self._model = model self._model = model
def json(self) -> str: def dump_json(self) -> str:
if isinstance(self._model, list): if isinstance(self._model, list):
return json.dumps( return json.dumps(
[item.model_dump(by_alias=True) for item in self._model], [item.model_dump(by_alias=True) for item in self._model],
@@ -24,6 +24,11 @@ class ModelView(Generic[TModel]):
) )
return self._model.model_dump_json(by_alias=True) return self._model.model_dump_json(by_alias=True)
def dump(self) -> dict | list:
if isinstance(self._model, list):
return [item.model_dump(by_alias=True) for item in self._model]
return self._model.model_dump(by_alias=True)
def __iter__(self) -> Iterator[TModel]: def __iter__(self) -> Iterator[TModel]:
if isinstance(self._model, list): if isinstance(self._model, list):
return iter(self._model) return iter(self._model)
@@ -67,8 +72,11 @@ class NodeConfig:
def text(self): def text(self):
return self._response.text return self._response.text
def json(self): def dump_json(self):
return self._parsed_data.model_dump_json() return self._parsed_data.model_dump_json(by_alias=True)
def dump(self):
return self._parsed_data.model_dump(by_alias=True)
def __str__(self): def __str__(self):
return self.text return self.text

View File

@@ -1,5 +1,8 @@
from typing import Optional from typing import Optional
from requests import Session from requests import HTTPError, Session
from oxi.adapter import OxiAdapter
from oxi.exception import OxiAPIError
from .node import Node from .node import Node
@@ -12,12 +15,24 @@ class OxiAPI:
verify: bool = True, verify: bool = True,
): ):
self.base_url = url.rstrip("/") self.base_url = url.rstrip("/")
self._session = Session() self._session = self.__create_session(username, password, verify)
self._session.verify = verify
if username and password:
self._session.auth = (username, password)
self.node = Node(self._session, self.base_url) self.node = Node(self._session, self.base_url)
def __create_session(
self,
username: Optional[str] = None,
password: Optional[str] = None,
verify: bool = True,
) -> Session:
session = Session()
adapter = OxiAdapter(timeout=10, max_retries=3)
session.mount("https://", adapter)
session.mount("http://", adapter)
session.verify = verify
if username and password:
session.auth = (username, password)
return session
def __enter__(self): def __enter__(self):
return self return self
@@ -26,3 +41,11 @@ class OxiAPI:
def close(self): def close(self):
return self._session.close() return self._session.close()
def reload(self):
try:
reload_response = self._session.get(f"{self.base_url}/reload")
reload_response.raise_for_status()
except HTTPError as e:
raise OxiAPIError.from_http_error(e, context="Reload Oxidized") from e
return reload_response.status_code

34
oxi/exception.py Normal file
View File

@@ -0,0 +1,34 @@
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from requests import HTTPError
_STATUS_MESSAGES: dict[int, str] = {
401: "Unauthorized",
403: "Forbidden",
500: "Internal Server Error",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
}
class OxiAPIError(Exception):
def __init__(self, message: str, status_code: Optional[int] = None):
super().__init__(message)
self.status_code = status_code
def __str__(self):
if self.status_code is not None:
return f"OxiAPIError: {self.args[0]} (HTTP {self.status_code})"
return f"OxiAPIError: {self.args[0]}"
@classmethod
def from_http_error(cls, e: "HTTPError", context: str = "") -> "OxiAPIError":
status = e.response.status_code
if status == 404:
message = f"{context} not found" if context else "Not found"
else:
base = _STATUS_MESSAGES.get(status) or e.response.reason or f"HTTP {status}"
message = f"{context}: {base}" if context else base
return cls(message, status)

View File

@@ -8,7 +8,7 @@ class Base(BaseModel):
class System(BaseModel): class System(BaseModel):
""" """
Requred Required
""" """
model: str model: str
@@ -18,7 +18,7 @@ class System(BaseModel):
class Interfaces(Base): class Interfaces(Base):
""" """
Requred Required
""" """
name: str = Field(alias="interface") name: str = Field(alias="interface")
@@ -32,7 +32,7 @@ class Vlans(Base):
Optional Optional
""" """
vlan_id: int = Field(alias="id") vlan_id: int
name: str | None = Field(default=None, alias="description") name: str | None = Field(default=None, alias="description")

View File

@@ -7,6 +7,10 @@ from oxi.interfaces.base import BaseDevice
class Qtech(BaseDevice): class Qtech(BaseDevice):
template = "qtech.ttp" template = "qtech.ttp"
def system(self) -> dict:
system = self.raw["system"]
return system
def vlans(self) -> list[dict]: def vlans(self) -> list[dict]:
vlans_ttp = self.raw["vlans"] vlans_ttp = self.raw["vlans"]
vlans = [] vlans = []

View File

@@ -0,0 +1,36 @@
from oxi.interfaces import BaseDevice, register_parser
@register_parser(["quasar", "qos"])
class Quasar(BaseDevice):
template = "quasar.ttp"
def interfaces(self) -> list[dict]:
ether_interfaces: dict = self.raw["interfaces"]
interfaces: list[dict] = []
bulk_interfaces: dict = self.raw["bulkinterfaces"]
for key, value in bulk_interfaces.items():
interfaces.append(
{
"interface": key,
"description": value.get("description"),
"ip_address": value.get("ip_address"),
"mask": value.get("mask"),
}
)
interfaces.append(ether_interfaces)
return interfaces
if __name__ == "__main__":
with open("./test7.txt") as file:
data = file.read()
quasar = Quasar(data)
qt = quasar.parse()
print(qt)
print()
with open("./test8.txt") as file:
data = file.read()
quasar = Quasar(data)
qt = quasar.parse()
print(qt)

View File

@@ -9,18 +9,23 @@ default_system = {
</vars> </vars>
<group name="system" default="default_system"> <group name="system" default="default_system">
! {{ model | ORPHRASE }} Series Software, Version {{ ignore }} Build {{ version | strip(",") }}{{ ignore('.*') }} ! {{ model | ORPHRASE | _start_ }} Series Software, Version {{ ignore }} Build {{ version | strip(",") }}{{ ignore('.*') }}
! Serial num:{{ serial_number | strip(",") }}{{ ignore('.*') }} ! Serial num:{{ serial_number | strip(",") }}{{ ignore('.*') }}
! System description : {{ description | PHRASE | _start_ }}({{ model }}) By {{ vendor }}
! System description : {{ description | PHRASE | _start_ }}({{ model }})
! System software version : {{ description | PHRASE }}, Release({{ version }})
! System serial number : {{ serial_number }}
</group> </group>
<group name="interfaces"> <group name="interfaces">
interface {{ interface }} interface {{ interface | ORPHRASE }}
description {{ description | ORPHRASE }} description {{ description | ORPHRASE }}
ip address {{ ip_address }} {{ mask | to_cidr }} ip address {{ ip_address }} {{ mask | to_cidr }}
</group> </group>
<group name="vlans"> <group name="vlans">
vlan {{ vlan_ids | contains(",", "-") | unrange("-", ",") | _start_ }} vlan {{ vlan_ids | contains(",", "-") | unrange("-", ",") }}
,{{ vlan_tail | unrange("-", ",") }} ,{{ vlan_tail | unrange("-", ",") }}
vlan {{ vlan_id | _start_ }} vlan {{ vlan_id | _start_ }}
name {{ name | ORPHRASE }} name {{ name | ORPHRASE }}

View File

@@ -0,0 +1,66 @@
<doc>
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
в &lt;vendor&gt;.ttp и заполните группы под формат конфигурации вашего устройства.
Обязательные группы: system, interfaces.
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
--- Группа system ---
Должна возвращать одиночный словарь с полями:
model (str) — модель устройства
serial_number (str) — серийный номер
version (str) — версия прошивки
--- Группа interfaces ---
Должна возвращать список словарей. Каждый элемент:
interface (str) — имя интерфейса (alias поля name)
ip_address (str|None) — IPv4-адрес
mask (int|None) — длина префикса (напр. 24)
description (str|None) — описание интерфейса
Если устройство возвращает маску в виде 255.255.255.0, конвертируйте
её в prefix length в методе interfaces() класса устройства.
--- Группа vlans ---
Должна возвращать список словарей. Каждый элемент:
id (int) — номер VLAN (alias поля vlan_id)
description (str|None) — название VLAN (alias поля name)
--- Полезные модификаторы TTP ---
{{ field | ORPHRASE }} — одно слово или фраза до конца строки
{{ field | _start_ }} — начало новой записи группы
{{ field | strip('"') }} — убрать кавычки
{{ field | replace("yes","True") }} — замена подстроки
{{ field | exclude("pattern") }} — пропустить строку при совпадении
{{ ignore }} — захватить и выбросить значение
{{ ignore('.*') }} — выбросить всё до конца строки
Подробнее: docs/templates.md
</doc>
<vars>
default_system = {
"model": "",
"serial_number": "",
"version": ""
}
</vars>
<group name="system" default="default_system">
# Component Version {{ _start_ }}
# Assembly {{ version }}
# Product Name {{ model | ORPHRASE }}
# Product Serial Number {{ serial_number }}
# Subsystem Version {{ _start_ }}
# Engine {{ version }}
# Product Name {{ model | ORPHRASE }}
# Product Serial Number {{ serial_number }}
</group>
<group name="interfaces">
{{ ignore("\\s*") }}config {{ interface }} ipv4 address {{ ip_address }}/{{ mask }} gateway {{ gateway }} {{ ignore }}
</group>
<group name="bulkinterfaces.{{ interface }}">
{{ ignore("\\s*") }}config interface {{ interface | _start_ }} description {{ description | ORPHRASE | strip('"')}}
</group>

View File

@@ -1,5 +1,9 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from requests import HTTPError
from oxi.exception import OxiAPIError
from .view import NodeView from .view import NodeView
@@ -11,15 +15,16 @@ class Node:
def __init__(self, session: "Session", base_url: str): def __init__(self, session: "Session", base_url: str):
self._session = session self._session = session
self._base_url = base_url self._base_url = base_url
self._data = None
def __call__(self, name: str) -> NodeView: def __call__(self, name: str) -> NodeView:
url = f"{self._base_url}/node/show/{name}" try:
if not url.endswith(".json"): url = f"{self._base_url}/node/show/{name}"
url += ".json" if not url.endswith(".json"):
response = self._session.get(url) url += ".json"
if response.status_code == 500: response = self._session.get(url)
raise ValueError(f"page {url} not found") response.raise_for_status()
except HTTPError as e:
raise OxiAPIError.from_http_error(e, context=f"Node {name}") from e
return NodeView( return NodeView(
session=self._session, base_url=self._base_url, data=response.json() session=self._session, base_url=self._base_url, data=response.json()
) )

View File

@@ -14,22 +14,47 @@ class NodeView:
self._base_url = base_url self._base_url = base_url
self._data = data self._data = data
def _updater(self) -> int:
response = self._session.get(f"{self._base_url}/node/next/{self.full_name}")
response.raise_for_status()
return response.status_code
@property @property
def ip(self): def name(self) -> str:
return self._data.get("name")
@property
def ip(self) -> str:
return self._data.get("ip") return self._data.get("ip")
@property @property
def full_name(self): def full_name(self) -> str:
return self._data.get("full_name") return self._data.get("full_name")
@property @property
def group(self): def group(self) -> str:
return self._data.get("group") return self._data.get("group")
@property @property
def model(self): def model(self) -> str:
return self._data.get("model") return self._data.get("model")
@property
def last_status(self) -> str:
last = self._data.get("last") or {}
return last.get("status")
@property
def last_check(self) -> str:
last = self._data.get("last") or {}
return last.get("start")
def refresh(self) -> str:
result = self._updater()
if result != 200:
raise ValueError(f"Failed to refresh node {self.full_name}")
return "OK"
@cached_property @cached_property
def config(self): def config(self) -> NodeConfig:
return NodeConfig(self._session, self.full_name, self.model, self._base_url) return NodeConfig(self._session, self.full_name, self.model, self._base_url)

View File

@@ -5,14 +5,27 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "oxipy" name = "oxipy"
version = "0.1.0" version = "0.1.0"
description = "Oxi API client" description = "Oxidized API client"
readme = "README.md" readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.11" requires-python = ">=3.11"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
]
dependencies = [ dependencies = [
"pydantic>=2.12.5", "pydantic>=2.12.5",
"requests>=2.32.5", "requests>=2.32.5",
"ttp>=0.10.0", "ttp>=0.10.0",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["."] where = ["."]
include = ["oxi*"] include = ["oxi*"]
[tool.setuptools]
include-package-data = true
[tool.setuptools.package-data]
"oxi" = ["**/*.ttp"]