UGOS 1.0.0__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.
Files changed (67) hide show
  1. ugos/__about__.py +6 -0
  2. ugos/__init__.py +92 -0
  3. ugos/_auth.py +293 -0
  4. ugos/_client_base.py +52 -0
  5. ugos/_client_calls.py +200 -0
  6. ugos/_client_helpers.py +217 -0
  7. ugos/_client_lifecycle.py +144 -0
  8. ugos/_client_resources.py +192 -0
  9. ugos/_client_transfer.py +501 -0
  10. ugos/_codec.py +528 -0
  11. ugos/_config.py +183 -0
  12. ugos/_data/__init__.py +1 -0
  13. ugos/_data/action_contracts.json +325 -0
  14. ugos/_data/catalog.json +1256 -0
  15. ugos/_dynamic.py +221 -0
  16. ugos/_encoding.py +159 -0
  17. ugos/_pagination.py +158 -0
  18. ugos/_redaction.py +158 -0
  19. ugos/_transport.py +446 -0
  20. ugos/catalog.py +184 -0
  21. ugos/client.py +113 -0
  22. ugos/exceptions.py +137 -0
  23. ugos/models/__init__.py +37 -0
  24. ugos/models/common.py +87 -0
  25. ugos/models/entities.py +124 -0
  26. ugos/models/users.py +43 -0
  27. ugos/py.typed +0 -0
  28. ugos/resources/__init__.py +55 -0
  29. ugos/resources/administrators.py +71 -0
  30. ugos/resources/application.py +107 -0
  31. ugos/resources/base.py +38 -0
  32. ugos/resources/certificates.py +62 -0
  33. ugos/resources/contentrules.py +98 -0
  34. ugos/resources/contenttypes.py +9 -0
  35. ugos/resources/fwrules.py +103 -0
  36. ugos/resources/generic.py +283 -0
  37. ugos/resources/generic_controllers.py +169 -0
  38. ugos/resources/groups.py +94 -0
  39. ugos/resources/icap.py +132 -0
  40. ugos/resources/idps.py +125 -0
  41. ugos/resources/interfaces.py +40 -0
  42. ugos/resources/l7.py +110 -0
  43. ugos/resources/licensing.py +39 -0
  44. ugos/resources/mailsecurity.py +101 -0
  45. ugos/resources/networks.py +82 -0
  46. ugos/resources/netzones.py +78 -0
  47. ugos/resources/nlist.py +72 -0
  48. ugos/resources/overridedomains.py +47 -0
  49. ugos/resources/overrides.py +109 -0
  50. ugos/resources/overrides_content.py +254 -0
  51. ugos/resources/overrides_content_a.py +187 -0
  52. ugos/resources/overrides_lists.py +294 -0
  53. ugos/resources/overrides_misc.py +342 -0
  54. ugos/resources/overrides_network.py +291 -0
  55. ugos/resources/pcap.py +64 -0
  56. ugos/resources/registry.py +55 -0
  57. ugos/resources/settings.py +72 -0
  58. ugos/resources/sysinfo.py +39 -0
  59. ugos/resources/twofa.py +62 -0
  60. ugos/resources/urlcategory.py +69 -0
  61. ugos/resources/urlfiltering.py +23 -0
  62. ugos/resources/urllists.py +9 -0
  63. ugos/resources/users.py +74 -0
  64. ugos-1.0.0.dist-info/METADATA +139 -0
  65. ugos-1.0.0.dist-info/RECORD +67 -0
  66. ugos-1.0.0.dist-info/WHEEL +4 -0
  67. ugos-1.0.0.dist-info/licenses/LICENSE +16 -0
@@ -0,0 +1,217 @@
1
+ """Internal helpers shared by the client implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import os
7
+ import re
8
+ from collections.abc import Collection, Iterator, Mapping
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import httpx
13
+
14
+ from .exceptions import DownloadError
15
+
16
+ _VERIFY_FALSE = frozenset({"0", "false", "no", "off"})
17
+ _VERIFY_TRUE = frozenset({"1", "true", "yes", "on"})
18
+
19
+
20
+ def _parse_verify(value: str) -> bool | str:
21
+ lowered = value.strip().lower()
22
+ if lowered in _VERIFY_FALSE:
23
+ return False
24
+ if lowered in _VERIFY_TRUE:
25
+ return True
26
+ return value
27
+
28
+
29
+ @dataclasses.dataclass
30
+ class _RetryState:
31
+ csrf: int = 0
32
+ reauth: int = 0
33
+
34
+
35
+ @dataclasses.dataclass
36
+ class _DownloadSpec:
37
+ controller: str
38
+ action: str
39
+ path: str
40
+ method: str
41
+ params: Mapping[str, Any]
42
+ json_fields: Collection[str]
43
+ timeout: Any
44
+ chunk_size: int
45
+ limit: int | None
46
+ dest: str | Path | None
47
+ filename: str | None
48
+ overwrite: bool
49
+
50
+
51
+ def _retry_allowed(action: str, idempotent: bool, exc: Exception) -> bool:
52
+ """Allow automatic replay only for explicitly idempotent actions."""
53
+ del action, exc
54
+ return idempotent
55
+
56
+
57
+ def _merge_params(payload: Mapping[str, Any] | None, params: Mapping[str, Any]) -> dict[str, Any]:
58
+ merged = dict(payload or {})
59
+ overlap = set(merged) & set(params)
60
+ if overlap:
61
+ names = ", ".join(sorted(overlap))
62
+ raise TypeError(f"duplicate parameters: {names}")
63
+ merged.update(params)
64
+ return merged
65
+
66
+
67
+ def _multipart_fields(pairs: list[tuple[str, str]]) -> dict[str, Any]:
68
+ fields: dict[str, Any] = {}
69
+ for key, value in pairs:
70
+ if key not in fields:
71
+ fields[key] = value
72
+ elif isinstance(fields[key], list):
73
+ fields[key].append(value)
74
+ else:
75
+ fields[key] = [fields[key], value]
76
+ return fields
77
+
78
+
79
+ def _raise_if_too_large(
80
+ total: int,
81
+ limit: int | None,
82
+ controller: str,
83
+ action: str,
84
+ ) -> None:
85
+ if limit is None or total <= limit:
86
+ return
87
+ raise DownloadError(
88
+ f"download exceeds {limit} bytes",
89
+ controller=controller,
90
+ action=action,
91
+ )
92
+
93
+
94
+ def _limited_chunks(
95
+ first: bytes,
96
+ chunks: Iterator[bytes],
97
+ *,
98
+ limit: int | None,
99
+ controller: str,
100
+ action: str,
101
+ ) -> Iterator[bytes]:
102
+ total = len(first)
103
+ _raise_if_too_large(total, limit, controller, action)
104
+ if first:
105
+ yield first
106
+ for chunk in chunks:
107
+ total += len(chunk)
108
+ _raise_if_too_large(total, limit, controller, action)
109
+ if chunk:
110
+ yield chunk
111
+
112
+
113
+ def _is_file_tuple(value: Any) -> bool:
114
+ if not isinstance(value, (list, tuple)) or len(value) < 2:
115
+ return False
116
+ if not isinstance(value[0], (str, bytes)):
117
+ return False
118
+ return callable(getattr(value[1], "read", None))
119
+
120
+
121
+ def _iter_file_objects(value: Any) -> Iterator[Any]:
122
+ if isinstance(value, (list, tuple)):
123
+ if _is_file_tuple(value):
124
+ yield value[1]
125
+ return
126
+ for item in value:
127
+ yield from _iter_file_objects(item)
128
+ return
129
+ if callable(getattr(value, "read", None)):
130
+ yield value
131
+
132
+
133
+ def _file_position(file_object: Any) -> int | object:
134
+ tell = getattr(file_object, "tell", None)
135
+ seek = getattr(file_object, "seek", None)
136
+ if not callable(tell) or not callable(seek):
137
+ return _NO_POSITION
138
+ try:
139
+ return int(tell())
140
+ except (OSError, ValueError):
141
+ return _NO_POSITION
142
+
143
+
144
+ def _capture_file_positions(
145
+ files: Mapping[str, Any],
146
+ ) -> list[tuple[Any, int]] | None:
147
+ positions: list[tuple[Any, int]] = []
148
+ for value in files.values():
149
+ for file_object in _iter_file_objects(value):
150
+ position = _file_position(file_object)
151
+ if position is _NO_POSITION:
152
+ return None
153
+ positions.append((file_object, position)) # type: ignore[arg-type]
154
+ return positions
155
+
156
+
157
+ def _restore_file_positions(positions: list[tuple[Any, int]]) -> None:
158
+ for file_object, position in positions:
159
+ file_object.seek(position)
160
+
161
+
162
+ def _filename_from_response(response: httpx.Response, path: str) -> str:
163
+ disposition = response.headers.get("content-disposition", "")
164
+ if disposition:
165
+ match = re.search(r"filename\*=UTF-8''([^;]+)", disposition, re.IGNORECASE)
166
+ if match:
167
+ from urllib.parse import unquote
168
+
169
+ return unquote(match.group(1).strip().strip('"'))
170
+ match = re.search(r'filename="?([^";]+)"?', disposition, re.IGNORECASE)
171
+ if match:
172
+ return match.group(1).strip()
173
+ return Path(path).name or "download.bin"
174
+
175
+
176
+ def _is_directory_destination(text: str, path: Path) -> bool:
177
+ if path.is_dir():
178
+ return True
179
+ if text.endswith(("/", os.sep)):
180
+ return True
181
+ altsep = os.altsep
182
+ return altsep is not None and text.endswith(altsep)
183
+
184
+
185
+ _ERROR_BODY_LIMIT = 2 * 1024 * 1024
186
+
187
+ _NO_POSITION = object()
188
+
189
+ _WINDOWS_RESERVED = {
190
+ "CON",
191
+ "PRN",
192
+ "AUX",
193
+ "NUL",
194
+ *(f"COM{i}" for i in range(1, 10)),
195
+ *(f"LPT{i}" for i in range(1, 10)),
196
+ }
197
+
198
+
199
+ def _error_body_limit(limit: int | None) -> int:
200
+ return _ERROR_BODY_LIMIT if limit is None else min(limit, _ERROR_BODY_LIMIT)
201
+
202
+
203
+ def _sanitize_filename(name: str) -> str:
204
+ clean = Path(name.replace("\\", "/")).name
205
+ clean = re.sub(r"[\x00-\x1f]", "", clean)
206
+ clean = clean.replace(":", "_").strip().rstrip(" .")
207
+ if clean in {"", ".", ".."} or clean.split(".")[0].upper() in _WINDOWS_RESERVED:
208
+ return "download.bin"
209
+ return clean[:255]
210
+
211
+
212
+ def _resolve_destination(dest: str | Path, filename: str) -> Path:
213
+ text = str(dest)
214
+ path = Path(dest)
215
+ if _is_directory_destination(text, path):
216
+ return path / filename
217
+ return path
@@ -0,0 +1,144 @@
1
+ """Client lifecycle, environment and session methods."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import warnings
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any, cast
9
+
10
+ from .__about__ import SUPPORTED_MAJOR_VERSION
11
+ from ._client_base import _ClientBase
12
+ from ._client_helpers import _parse_verify
13
+ from ._config import DEFAULT_TIMEOUT
14
+ from .catalog import Catalog
15
+ from .exceptions import ConfigurationError
16
+ from .models.common import SessionInfo
17
+
18
+ if TYPE_CHECKING:
19
+ from .client import UGOSClient
20
+
21
+
22
+ def _major_version(version: str | None) -> int | None:
23
+ if not version:
24
+ return None
25
+ head = version.strip().lstrip("vV").split(".", 1)[0]
26
+ try:
27
+ return int(head)
28
+ except ValueError:
29
+ return None
30
+
31
+
32
+ class _LifecycleMixin(_ClientBase):
33
+ @staticmethod
34
+ def _env_verify(overrides: dict[str, Any]) -> bool | str | Path:
35
+ if "verify" in overrides:
36
+ return overrides.pop("verify")
37
+ ca_bundle = os.getenv("UGOS_CA_BUNDLE")
38
+ if ca_bundle:
39
+ return ca_bundle
40
+ verify_env = os.getenv("UGOS_VERIFY")
41
+ if not verify_env:
42
+ return True
43
+ return _parse_verify(verify_env)
44
+
45
+ @staticmethod
46
+ def _env_timeout(overrides: dict[str, Any]) -> float | None:
47
+ if "timeout" in overrides:
48
+ return overrides.pop("timeout")
49
+ timeout_env = os.getenv("UGOS_TIMEOUT")
50
+ if not timeout_env:
51
+ return DEFAULT_TIMEOUT
52
+ try:
53
+ return float(timeout_env)
54
+ except ValueError as exc:
55
+ raise ConfigurationError("UGOS_TIMEOUT must be a number") from exc
56
+
57
+ @staticmethod
58
+ def _env_insecure(overrides: dict[str, Any]) -> None:
59
+ if "allow_insecure_http" in overrides:
60
+ return
61
+ insecure = os.getenv("UGOS_ALLOW_INSECURE_HTTP", "").strip().lower()
62
+ overrides["allow_insecure_http"] = insecure in {"1", "true", "yes", "on"}
63
+
64
+ @classmethod
65
+ def from_env(cls, **overrides: Any) -> UGOSClient:
66
+ """Create a client from ``UGOS_*`` environment variables."""
67
+
68
+ base_url = overrides.pop("base_url", None) or os.getenv("UGOS_URL")
69
+ if not base_url:
70
+ raise ConfigurationError("UGOS_URL or base_url is required")
71
+ username = overrides.pop("username", None) or os.getenv("UGOS_USERNAME")
72
+ password = overrides.pop("password", None) or os.getenv("UGOS_PASSWORD")
73
+ verify = cls._env_verify(overrides)
74
+ timeout = cls._env_timeout(overrides)
75
+ cls._env_insecure(overrides)
76
+ return cast("type[UGOSClient]", cls)(
77
+ base_url=base_url,
78
+ username=username,
79
+ password=password,
80
+ verify=verify,
81
+ timeout=timeout,
82
+ **overrides,
83
+ )
84
+
85
+ @property
86
+ def base_url(self) -> str:
87
+ return self._config.base_url
88
+
89
+ @property
90
+ def session(self) -> SessionInfo | None:
91
+ return self._auth.session
92
+
93
+ @property
94
+ def is_authenticated(self) -> bool:
95
+ return self._auth.authenticated
96
+
97
+ @property
98
+ def catalog(self) -> Catalog:
99
+ return self._catalog
100
+
101
+ def login(
102
+ self,
103
+ username: str | None = None,
104
+ password: str | None = None,
105
+ *,
106
+ force: bool = False,
107
+ ) -> SessionInfo:
108
+ with self._lock:
109
+ if username is not None:
110
+ self._auth._username = username
111
+ if password is not None:
112
+ self._auth._password = password
113
+ session = self._auth.login(force=force)
114
+ major = _major_version(session.version)
115
+ if session.version and major != SUPPORTED_MAJOR_VERSION:
116
+ warnings.warn(
117
+ f"NGFW version {session.version} is outside the supported major version "
118
+ f"{SUPPORTED_MAJOR_VERSION}",
119
+ RuntimeWarning,
120
+ stacklevel=2,
121
+ )
122
+ return session
123
+
124
+ def logout(self, *, best_effort: bool = True) -> bool:
125
+ with self._lock:
126
+ return self._auth.logout(best_effort=best_effort)
127
+
128
+ def close(self) -> None:
129
+ self._transport.close()
130
+
131
+ def __enter__(self) -> UGOSClient:
132
+ return cast("UGOSClient", self)
133
+
134
+ def __exit__(self, _exc_type: Any, _exc: Any, _tb: Any) -> None:
135
+ if self._logout_on_exit and self._auth.authenticated:
136
+ self.logout(best_effort=True)
137
+ self.close()
138
+
139
+ def __repr__(self) -> str:
140
+ username = self._auth.session.username if self._auth.session else None
141
+ return (
142
+ f"UGOSClient(base_url={self.base_url!r}, username={username!r}, "
143
+ f"password='***', session='***')"
144
+ )
@@ -0,0 +1,192 @@
1
+ """Typed and dynamic controller resource access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ._client_base import _T, _ClientBase
8
+ from ._client_base import CONTROLLER_RE as _CONTROLLER_RE
9
+ from ._dynamic import ControllerProxy
10
+ from .exceptions import ConfigurationError, UnknownControllerError
11
+ from .resources.administrators import AdministratorsResource
12
+ from .resources.application import ApplicationResource
13
+ from .resources.certificates import CertificatesResource
14
+ from .resources.contentrules import ContentRulesResource
15
+ from .resources.contenttypes import ContentTypesResource
16
+ from .resources.fwrules import FirewallRulesResource
17
+ from .resources.generic_controllers import GENERIC_RESOURCES
18
+ from .resources.groups import GroupsResource
19
+ from .resources.icap import IcapResource
20
+ from .resources.idps import IdpsResource
21
+ from .resources.interfaces import InterfacesResource
22
+ from .resources.l7 import L7Resource
23
+ from .resources.licensing import LicensingResource
24
+ from .resources.mailsecurity import MailSecurityResource
25
+ from .resources.networks import NetworksResource
26
+ from .resources.netzones import NetzonesResource
27
+ from .resources.overridedomains import OverrideDomainsResource
28
+ from .resources.overrides import TYPED_OVERRIDES
29
+ from .resources.pcap import PcapResource
30
+ from .resources.registry import TYPED_RESOURCES
31
+ from .resources.settings import SettingsResource
32
+ from .resources.sysinfo import SysinfoResource
33
+ from .resources.twofa import TwoFactorResource
34
+ from .resources.urlcategory import UrlCategoryResource
35
+ from .resources.urlfiltering import UrlFilteringResource
36
+ from .resources.urllists import UrlListsResource
37
+ from .resources.users import UsersResource
38
+
39
+
40
+ class _ResourcesMixin(_ClientBase):
41
+ @staticmethod
42
+ def _resource_class(name: str) -> Any:
43
+ return TYPED_RESOURCES.get(name) or TYPED_OVERRIDES.get(name) or GENERIC_RESOURCES.get(name)
44
+
45
+ def _dynamic_controller(self, name: str, strict: bool | None) -> ControllerProxy:
46
+ with self._lock:
47
+ cached = self._resources.get(name)
48
+ if cached is None:
49
+ cached = ControllerProxy(self, name, strict=strict)
50
+ self._resources[name] = cached
51
+ elif strict is not None:
52
+ cached._strict = strict
53
+ return cached
54
+
55
+ def controller(self, name: str, *, strict: bool | None = None) -> ControllerProxy:
56
+ if not _CONTROLLER_RE.match(name):
57
+ raise ConfigurationError(f"invalid controller name: {name!r}")
58
+ resource_cls = self._resource_class(name)
59
+ if resource_cls is not None:
60
+ return self._resource(name, resource_cls, strict=strict)
61
+ return self._dynamic_controller(name, strict)
62
+
63
+ def available_actions(self, controller: str) -> frozenset[str]:
64
+ return self._catalog.actions(controller)
65
+
66
+ def has_action(self, controller: str, action: str) -> bool:
67
+ return self._catalog.has_action(controller, action)
68
+
69
+ def list_actions(self, controller: str) -> frozenset[str]:
70
+ try:
71
+ return self._catalog.actions(controller)
72
+ except UnknownControllerError:
73
+ return frozenset()
74
+
75
+ def __getattr__(self, name: str) -> ControllerProxy:
76
+ if name.startswith("_"):
77
+ raise AttributeError(name)
78
+ if self._catalog_check and not self._catalog.has_controller(name):
79
+ raise AttributeError(f"unknown controller: {name}")
80
+ return self.controller(name)
81
+
82
+ @property
83
+ def administrators(self) -> AdministratorsResource:
84
+ return self._resource("administrators", AdministratorsResource)
85
+
86
+ @property
87
+ def users(self) -> UsersResource:
88
+ return self._resource("users", UsersResource)
89
+
90
+ @property
91
+ def groups(self) -> GroupsResource:
92
+ return self._resource("groups", GroupsResource)
93
+
94
+ @property
95
+ def fwrules(self) -> FirewallRulesResource:
96
+ return self._resource("fwrules", FirewallRulesResource)
97
+
98
+ @property
99
+ def netzones(self) -> NetzonesResource:
100
+ return self._resource("netzones", NetzonesResource)
101
+
102
+ @property
103
+ def interfaces(self) -> InterfacesResource:
104
+ return self._resource("interfaces", InterfacesResource)
105
+
106
+ @property
107
+ def networks(self) -> NetworksResource:
108
+ return self._resource("networks", NetworksResource)
109
+
110
+ @property
111
+ def settings(self) -> SettingsResource:
112
+ return self._resource("settings", SettingsResource)
113
+
114
+ @property
115
+ def certificates(self) -> CertificatesResource:
116
+ return self._resource("certificate", CertificatesResource)
117
+
118
+ @property
119
+ def licensing(self) -> LicensingResource:
120
+ return self._resource("utm", LicensingResource)
121
+
122
+ @property
123
+ def sysinfo(self) -> SysinfoResource:
124
+ return self._resource("sysinfo", SysinfoResource)
125
+
126
+ @property
127
+ def pcap(self) -> PcapResource:
128
+ return self._resource("pcap", PcapResource)
129
+
130
+ @property
131
+ def twofa(self) -> TwoFactorResource:
132
+ return self._resource("2fa", TwoFactorResource)
133
+
134
+ @property
135
+ def contentrules(self) -> ContentRulesResource:
136
+ return self._resource("contentrules", ContentRulesResource)
137
+
138
+ @property
139
+ def contenttypes(self) -> ContentTypesResource:
140
+ return self._resource("contenttypes", ContentTypesResource)
141
+
142
+ @property
143
+ def urlcategory(self) -> UrlCategoryResource:
144
+ return self._resource("urlcategory", UrlCategoryResource)
145
+
146
+ @property
147
+ def urlfiltering(self) -> UrlFilteringResource:
148
+ return self._resource("urlfiltering", UrlFilteringResource)
149
+
150
+ @property
151
+ def urllists(self) -> UrlListsResource:
152
+ return self._resource("urllists", UrlListsResource)
153
+
154
+ @property
155
+ def application(self) -> ApplicationResource:
156
+ return self._resource("application", ApplicationResource)
157
+
158
+ @property
159
+ def l7(self) -> L7Resource:
160
+ return self._resource("l7", L7Resource)
161
+
162
+ @property
163
+ def idps(self) -> IdpsResource:
164
+ return self._resource("idps", IdpsResource)
165
+
166
+ @property
167
+ def icap(self) -> IcapResource:
168
+ return self._resource("icap", IcapResource)
169
+
170
+ @property
171
+ def mailsecurity(self) -> MailSecurityResource:
172
+ return self._resource("mailsecurity", MailSecurityResource)
173
+
174
+ @property
175
+ def overridedomains(self) -> OverrideDomainsResource:
176
+ return self._resource("overridedomains", OverrideDomainsResource)
177
+
178
+ def _resource(
179
+ self,
180
+ key: str,
181
+ factory: type[_T],
182
+ *,
183
+ strict: bool | None = None,
184
+ ) -> _T:
185
+ with self._lock:
186
+ cached = self._resources.get(key)
187
+ if cached is None or not isinstance(cached, factory):
188
+ cached = factory(self, strict=strict)
189
+ self._resources[key] = cached
190
+ elif strict is not None:
191
+ cached._strict = strict
192
+ return cached