rackfish 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl

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.
rackfish/__init__.py CHANGED
@@ -19,7 +19,7 @@ https://github.com/thefrolov/rackfish
19
19
 
20
20
  from .client import RedfishClient, RedfishError, RedfishResource
21
21
 
22
- __version__ = "1.0.1"
22
+ __version__ = "1.0.3"
23
23
  __author__ = "Dmitrii Frolov"
24
24
  __email__ = "thefrolov@mts.ru"
25
25
  __license__ = "MIT"
rackfish/client.py CHANGED
@@ -7,6 +7,7 @@ import threading
7
7
  from typing import Any, ClassVar, Iterable
8
8
 
9
9
  import requests
10
+ import urllib3
10
11
 
11
12
  # HTTP status codes
12
13
  HTTP_OK = 200
@@ -74,11 +75,6 @@ class RedfishClient:
74
75
  default_headers: dict[str, str] | None = None,
75
76
  ):
76
77
  base = base_url.rstrip("/")
77
- # if not base.endswith("/redfish/v1"):
78
- # if base.endswith("/redfish"):
79
- # base = base + "/v1"
80
- # else:
81
- # base = base + "/redfish/v1"
82
78
 
83
79
  self.base_url = base
84
80
  self.username = username
@@ -88,6 +84,11 @@ class RedfishClient:
88
84
 
89
85
  self._http = requests.Session()
90
86
  self._http.verify = verify_ssl
87
+
88
+ # Suppress SSL warnings when verify_ssl is disabled
89
+ if not verify_ssl:
90
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
91
+
91
92
  self._http.headers.update(default_headers or {"Accept": "application/json"})
92
93
  if username and password and not use_session:
93
94
  self._http.auth = (username, password)
@@ -156,9 +157,12 @@ class RedfishClient:
156
157
  return None
157
158
  return None
158
159
 
159
- def patch(self, path: str, data: dict[str, Any]) -> None:
160
+ def patch(self, path: str, data: dict[str, Any], etag: str | None = None) -> None:
160
161
  url = _safe_join(self.base_url, path) if not path.startswith("http") else path
161
- resp = self._http.patch(url, json=data, timeout=self.timeout)
162
+ headers = {}
163
+ if etag:
164
+ headers["If-Match"] = etag
165
+ resp = self._http.patch(url, json=data, headers=headers, timeout=self.timeout)
162
166
  if resp.status_code not in (200, 204):
163
167
  raise RedfishError(f"PATCH {url} -> {resp.status_code} {resp.text}")
164
168
 
@@ -191,7 +195,27 @@ class RedfishClient:
191
195
 
192
196
  def __getattr__(self, name: str) -> Any:
193
197
  # Proxy unknown attributes to root (e.g., client.Systems)
194
- return getattr(self.root, name)
198
+ # If attribute doesn't exist but plural form exists with single member,
199
+ # return that member directly (e.g., client.System -> client.Systems[0])
200
+ try:
201
+ return getattr(self.root, name)
202
+ except AttributeError as exc:
203
+ # Try plural form for singular access convenience
204
+ plural_name = name + "s"
205
+ try:
206
+ collection = getattr(self.root, plural_name)
207
+ if (
208
+ hasattr(collection, "__len__")
209
+ and hasattr(collection, "__iter__")
210
+ and len(collection) == 1
211
+ ):
212
+ return next(iter(collection))
213
+ except (AttributeError, TypeError):
214
+ pass
215
+ # Re-raise original error
216
+ raise AttributeError(
217
+ f"'{type(self).__name__}' object has no attribute '{name}'"
218
+ ) from exc
195
219
 
196
220
 
197
221
  # ---------------------------
@@ -383,11 +407,28 @@ class RedfishResource:
383
407
  self._ensure_fetched()
384
408
  try:
385
409
  return object.__getattribute__(self, name)
386
- except AttributeError:
410
+ except AttributeError as exc:
387
411
  # Not an attribute; maybe a JSON property with non-identifier key
388
412
  if name in self._raw:
389
413
  return self._raw[name]
390
- raise
414
+
415
+ # Try plural form for singular access convenience
416
+ # e.g., resource.Chassis -> resource.Chassis[0] if len == 1
417
+ plural_name = name + "s"
418
+ try:
419
+ collection = object.__getattribute__(self, plural_name)
420
+ if (
421
+ hasattr(collection, "__len__")
422
+ and hasattr(collection, "__iter__")
423
+ and len(collection) == 1
424
+ ):
425
+ return next(iter(collection))
426
+ except (AttributeError, TypeError):
427
+ pass
428
+
429
+ raise AttributeError(
430
+ f"'{type(self).__name__}' object has no attribute '{name}'"
431
+ ) from exc
391
432
 
392
433
  def __setattr__(self, name: str, value: Any) -> None:
393
434
  # Allow setting simple existing properties via PATCH
@@ -400,7 +441,8 @@ class RedfishResource:
400
441
 
401
442
  if name in self._raw and not isinstance(self._raw[name], (dict, list)):
402
443
  # PATCH only simple properties by default; complex updates via .patch()
403
- self._client.patch(self._path, {name: value})
444
+ etag = self._raw.get("@odata.etag")
445
+ self._client.patch(self._path, {name: value}, etag=etag)
404
446
  self._raw[name] = value
405
447
  object.__setattr__(self, name, value)
406
448
  else:
@@ -458,7 +500,9 @@ class RedfishResource:
458
500
  def patch(self, updates: dict[str, Any]) -> None:
459
501
  if not self._path:
460
502
  raise RedfishError("PATCH requires a resource path")
461
- self._client.patch(self._path, updates)
503
+ self._ensure_fetched()
504
+ etag = self._raw.get("@odata.etag")
505
+ self._client.patch(self._path, updates, etag=etag)
462
506
  self.refresh()
463
507
 
464
508
  def delete(self) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rackfish
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: A lightweight, dynamic Python client for Redfish BMC APIs
5
5
  Author-email: Dmitrii Frolov <thefrolov@mts.ru>
6
6
  License: MIT
@@ -58,17 +58,6 @@ A lightweight, dynamic Python client for interacting with Redfish BMC (Baseboard
58
58
  - 📚 **Collection Support** - Iterate Redfish collections naturally
59
59
  - 🔐 **Flexible Auth** - Session tokens or Basic authentication
60
60
 
61
- ## Features
62
-
63
- - **Zero Dependencies** (except `requests`) - Minimal footprint
64
- - **Lazy Loading** - Resources fetched on-demand for performance
65
- - **Dynamic Attributes** - JSON properties become Python attributes
66
- - **OEM Surfacing** - Vendor extensions automatically accessible
67
- - **Links Surfacing** - Related resources directly navigable
68
- - **Action Validation** - Parameter validation using ActionInfo schemas
69
- - **Collection Support** - Iterate Redfish collections naturally
70
- - **Session & Basic Auth** - Flexible authentication options
71
-
72
61
  ## Installation
73
62
 
74
63
  ### From PyPI (recommended)
@@ -101,8 +90,11 @@ client = RedfishClient("https://bmc.example.com", "admin", "password",
101
90
  use_session=True, verify_ssl=False)
102
91
  root = client.connect()
103
92
 
104
- # Power control
105
- system = next(iter(client.Systems))
93
+ # Power control - multiple ways to access systems
94
+ system = next(iter(client.Systems)) # Traditional iteration
95
+ system = client.Systems.Members[0] # Direct member access
96
+ system = client.System # Singular form (if only one member!)
97
+
106
98
  system.Reset(ResetType="GracefulRestart")
107
99
 
108
100
  # Access OEM properties (auto-surfaced)
@@ -166,7 +158,8 @@ for temp in chassis.Thermal.Temperatures:
166
158
  print(f"{temp.Name}: {temp.ReadingCelsius}°C")
167
159
  ```
168
160
 
169
- See [EXAMPLES.md](EXAMPLES.md) for 100+ more examples covering:
161
+ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for 100+ more examples covering:
162
+
170
163
  - BIOS configuration
171
164
  - Certificate management
172
165
  - Virtual media (KVM)
@@ -175,24 +168,6 @@ See [EXAMPLES.md](EXAMPLES.md) for 100+ more examples covering:
175
168
  - SEL/log collection
176
169
  - And much more...
177
170
 
178
- ## Testing
179
-
180
- Run the test suite:
181
-
182
- ```bash
183
- # Install with dev dependencies
184
- pip install -e ".[dev]"
185
-
186
- # Run all tests
187
- pytest tests/
188
-
189
- # Run with coverage
190
- pytest --cov=rackfish tests/
191
-
192
- # Run specific test file
193
- pytest tests/test_common_usage.py
194
- ```
195
-
196
171
  ## Project Structure
197
172
 
198
173
  ```
@@ -356,7 +331,7 @@ See LICENSE file.
356
331
 
357
332
  ## Version
358
333
 
359
- Current version: 1.0.0
334
+ Current version: 1.0.3
360
335
 
361
336
  ## Requirements
362
337
 
@@ -0,0 +1,7 @@
1
+ rackfish/__init__.py,sha256=hYKyM58DkDSL2ReP0CI18RT-TuBcYTmDZp5gepUfUGk,866
2
+ rackfish/client.py,sha256=aHtQikXEEsI_jjq24-LE8mR2sCasdAlhbB2zRP97200,25048
3
+ rackfish-1.0.3.dist-info/licenses/LICENSE,sha256=RqhA3IjOD-4eEnG-vVcLLU1vdpVCUyCQpytsCP3P0Q0,1071
4
+ rackfish-1.0.3.dist-info/METADATA,sha256=CnnyEOogYJYjOyRAnBpcGw21jnEO66EXhIihdDuRpeE,10645
5
+ rackfish-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ rackfish-1.0.3.dist-info/top_level.txt,sha256=71o_wqNuMF4jtlGbXuI2-Epzd2a9oSizMmdcG3Sw_Mo,9
7
+ rackfish-1.0.3.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- rackfish/__init__.py,sha256=gXLovtZSL_Od0gBj5huedVcJ30ufDLRwYJxTV7eE7Q0,866
2
- rackfish/client.py,sha256=4eOyfQtqo9kbvzxiV2DqyeM6t4fgAnKbj3odpiS9dmY,23225
3
- rackfish-1.0.1.dist-info/licenses/LICENSE,sha256=RqhA3IjOD-4eEnG-vVcLLU1vdpVCUyCQpytsCP3P0Q0,1071
4
- rackfish-1.0.1.dist-info/METADATA,sha256=55T2anIwPlDVEw1vnYT5fuAs-ZzVN4Vs6F4tEdwq9qA,11215
5
- rackfish-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- rackfish-1.0.1.dist-info/top_level.txt,sha256=71o_wqNuMF4jtlGbXuI2-Epzd2a9oSizMmdcG3Sw_Mo,9
7
- rackfish-1.0.1.dist-info/RECORD,,