basaltic-sdk-python 0.1.1__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 (47) hide show
  1. basaltic/__init__.py +26 -0
  2. basaltic/_client.py +136 -0
  3. basaltic/_common.py +284 -0
  4. basaltic/_transport.py +196 -0
  5. basaltic/_version.py +3 -0
  6. basaltic/auth.py +231 -0
  7. basaltic/client.py +144 -0
  8. basaltic/config.py +151 -0
  9. basaltic/errors.py +73 -0
  10. basaltic/models/__init__.py +17 -0
  11. basaltic/models/audit.py +85 -0
  12. basaltic/models/billing.py +284 -0
  13. basaltic/models/catalog.py +35 -0
  14. basaltic/models/certificate.py +127 -0
  15. basaltic/models/compute.py +905 -0
  16. basaltic/models/dns.py +256 -0
  17. basaltic/models/iam.py +736 -0
  18. basaltic/models/kms.py +157 -0
  19. basaltic/models/loadbalancer.py +574 -0
  20. basaltic/models/network.py +985 -0
  21. basaltic/models/quota.py +30 -0
  22. basaltic/models/secrets.py +136 -0
  23. basaltic/models/storage.py +789 -0
  24. basaltic/models/telemetry.py +393 -0
  25. basaltic/models/workspace.py +734 -0
  26. basaltic/py.typed +0 -0
  27. basaltic/response.py +152 -0
  28. basaltic/services/__init__.py +1 -0
  29. basaltic/services/audit.py +211 -0
  30. basaltic/services/billing.py +777 -0
  31. basaltic/services/catalog.py +172 -0
  32. basaltic/services/certificate.py +395 -0
  33. basaltic/services/compute.py +2395 -0
  34. basaltic/services/dns.py +1079 -0
  35. basaltic/services/iam.py +3405 -0
  36. basaltic/services/kms.py +742 -0
  37. basaltic/services/loadbalancer.py +1727 -0
  38. basaltic/services/network.py +4691 -0
  39. basaltic/services/quota.py +75 -0
  40. basaltic/services/secrets.py +550 -0
  41. basaltic/services/storage.py +3059 -0
  42. basaltic/services/telemetry.py +1316 -0
  43. basaltic/services/workspace.py +3899 -0
  44. basaltic_sdk_python-0.1.1.dist-info/METADATA +211 -0
  45. basaltic_sdk_python-0.1.1.dist-info/RECORD +47 -0
  46. basaltic_sdk_python-0.1.1.dist-info/WHEEL +4 -0
  47. basaltic_sdk_python-0.1.1.dist-info/licenses/LICENSE +202 -0
basaltic/auth.py ADDED
@@ -0,0 +1,231 @@
1
+ """Cached credentials with thread-safe and asyncio single-flight refresh."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import base64
7
+ import json
8
+ import math
9
+ import threading
10
+ import time
11
+ from collections.abc import Callable
12
+ from typing import Protocol
13
+
14
+ import httpx
15
+
16
+ from .config import positive_timeout, validate_token, validate_url
17
+ from .errors import AuthenticationError
18
+
19
+
20
+ class TokenProvider(Protocol):
21
+ def get_token(self) -> str: ...
22
+
23
+
24
+ class AsyncTokenProvider(Protocol):
25
+ async def get_token(self) -> str: ...
26
+
27
+
28
+ class StaticToken:
29
+ def __init__(self, token: str) -> None:
30
+ self.__token = validate_token(token)
31
+
32
+ def get_token(self) -> str:
33
+ return self.__token
34
+
35
+
36
+ class AsyncStaticToken:
37
+ def __init__(self, token: str) -> None:
38
+ self.__token = validate_token(token)
39
+
40
+ async def get_token(self) -> str:
41
+ return self.__token
42
+
43
+
44
+ def _token_response(status: int, raw: bytes) -> tuple[str, float]:
45
+ data: object = None
46
+ try:
47
+ data = json.loads(raw)
48
+ except (ValueError, UnicodeDecodeError):
49
+ pass
50
+ if status != 200 or not isinstance(data, dict):
51
+ code = data.get("error", "") if isinstance(data, dict) else ""
52
+ raise AuthenticationError(
53
+ status,
54
+ code if code in ("invalid_client", "invalid_grant", "temporarily_unavailable") else "",
55
+ )
56
+ token, lifetime, kind = (
57
+ data.get("access_token"),
58
+ data.get("expires_in", 900),
59
+ data.get("token_type", "Bearer"),
60
+ )
61
+ valid = isinstance(token, str) and isinstance(kind, str) and kind.lower() == "bearer"
62
+ try:
63
+ seconds = float(lifetime)
64
+ if not math.isfinite(seconds) or seconds <= 0 or isinstance(lifetime, bool):
65
+ valid = False
66
+ if valid and isinstance(token, str):
67
+ validate_token(token)
68
+ except (ValueError, TypeError, OverflowError):
69
+ valid = False
70
+ if not valid or not isinstance(token, str):
71
+ raise AuthenticationError(status)
72
+ return token, seconds - min(300, seconds * 0.1)
73
+
74
+
75
+ def _request(key: str, secret: str, url: str, timeout: float) -> httpx.Request:
76
+ if not key or ":" in key or not secret:
77
+ raise ValueError("A valid access key ID and secret are required.")
78
+ encoded = base64.b64encode((key + ":" + secret).encode()).decode()
79
+ return httpx.Request(
80
+ "POST",
81
+ validate_url(url),
82
+ content=b"grant_type=client_credentials",
83
+ headers={
84
+ "Authorization": "Basic " + encoded,
85
+ "Accept": "application/json",
86
+ "Content-Type": "application/x-www-form-urlencoded",
87
+ },
88
+ extensions={"timeout": httpx.Timeout(positive_timeout(timeout)).as_dict()},
89
+ )
90
+
91
+
92
+ class ClientCredentials:
93
+ def __init__(
94
+ self,
95
+ *,
96
+ access_key_id: str,
97
+ secret_access_key: str,
98
+ token_url: str,
99
+ http_client: httpx.Client,
100
+ timeout: float = 30,
101
+ clock: Callable[[], float] = time.monotonic,
102
+ ) -> None:
103
+ # Validate without retaining the prepared request, whose repr may expose headers.
104
+ _request(access_key_id, secret_access_key, token_url, timeout)
105
+ self.__key, self.__secret, self.__url = access_key_id, secret_access_key, token_url
106
+ self._http, self._timeout, self._clock = http_client, timeout, clock
107
+ self._lock = threading.Lock()
108
+ self.__cached, self._refresh_at = "", 0.0
109
+
110
+ def get_token(self) -> str:
111
+ with self._lock:
112
+ if self.__cached and self._clock() < self._refresh_at:
113
+ return self.__cached
114
+ now = self._clock()
115
+ failure: AuthenticationError | None = None
116
+ response: httpx.Response | None = None
117
+ try:
118
+ response = self._http.send(
119
+ _request(self.__key, self.__secret, self.__url, self._timeout),
120
+ auth=None,
121
+ follow_redirects=False,
122
+ stream=True,
123
+ )
124
+ raw = bytearray()
125
+ for chunk in response.iter_bytes():
126
+ raw.extend(chunk[: 1048577 - len(raw)])
127
+ if len(raw) > 1048576:
128
+ break
129
+ if len(raw) > 1048576:
130
+ raise AuthenticationError(response.status_code)
131
+ token, ttl = _token_response(response.status_code, bytes(raw))
132
+ except (httpx.HTTPError, AuthenticationError) as error:
133
+ failure = (
134
+ AuthenticationError(error.status_code, error.error_code)
135
+ if isinstance(error, AuthenticationError)
136
+ else AuthenticationError()
137
+ )
138
+ finally:
139
+ if response is not None:
140
+ response.close()
141
+ if failure is not None:
142
+ raise failure
143
+ self.__cached, self._refresh_at = token, now + ttl
144
+ return token
145
+
146
+ def invalidate(self, rejected_token: str | None = None) -> None:
147
+ with self._lock:
148
+ if rejected_token is None or rejected_token == self.__cached:
149
+ self.__cached, self._refresh_at = "", 0.0
150
+
151
+
152
+ class AsyncClientCredentials:
153
+ def __init__(
154
+ self,
155
+ *,
156
+ access_key_id: str,
157
+ secret_access_key: str,
158
+ token_url: str,
159
+ http_client: httpx.AsyncClient,
160
+ timeout: float = 30,
161
+ clock: Callable[[], float] = time.monotonic,
162
+ ) -> None:
163
+ _request(access_key_id, secret_access_key, token_url, timeout)
164
+ self.__key, self.__secret, self.__url = access_key_id, secret_access_key, token_url
165
+ self._http, self._timeout, self._clock = http_client, timeout, clock
166
+ self.__cached, self._refresh_at = "", 0.0
167
+ self._pending: asyncio.Task[str] | None = None
168
+
169
+ async def get_token(self) -> str:
170
+ if self.__cached and self._clock() < self._refresh_at:
171
+ return self.__cached
172
+ # There is no await between checking and assigning the task; creation is
173
+ # atomic within the client's event loop. A caller's cancellation is isolated.
174
+ if self._pending is None:
175
+ task = asyncio.create_task(self._refresh())
176
+ self._pending = task
177
+
178
+ def done(finished: asyncio.Task[str]) -> None:
179
+ if self._pending is finished:
180
+ self._pending = None
181
+ if not finished.cancelled():
182
+ finished.exception()
183
+
184
+ task.add_done_callback(done)
185
+ return await asyncio.shield(self._pending)
186
+
187
+ def invalidate(self, rejected_token: str | None = None) -> None:
188
+ if rejected_token is None or rejected_token == self.__cached:
189
+ self.__cached, self._refresh_at = "", 0.0
190
+
191
+ async def aclose(self) -> None:
192
+ if self._pending is not None:
193
+ pending = self._pending
194
+ pending.cancel()
195
+ try:
196
+ await pending
197
+ except asyncio.CancelledError:
198
+ pass
199
+
200
+ async def _refresh(self) -> str:
201
+ now = self._clock()
202
+ failure: AuthenticationError | None = None
203
+ response: httpx.Response | None = None
204
+ try:
205
+ response = await self._http.send(
206
+ _request(self.__key, self.__secret, self.__url, self._timeout),
207
+ auth=None,
208
+ follow_redirects=False,
209
+ stream=True,
210
+ )
211
+ raw = bytearray()
212
+ async for chunk in response.aiter_bytes():
213
+ raw.extend(chunk[: 1048577 - len(raw)])
214
+ if len(raw) > 1048576:
215
+ break
216
+ if len(raw) > 1048576:
217
+ raise AuthenticationError(response.status_code)
218
+ token, ttl = _token_response(response.status_code, bytes(raw))
219
+ except (httpx.HTTPError, AuthenticationError) as error:
220
+ failure = (
221
+ AuthenticationError(error.status_code, error.error_code)
222
+ if isinstance(error, AuthenticationError)
223
+ else AuthenticationError()
224
+ )
225
+ finally:
226
+ if response is not None:
227
+ await response.aclose()
228
+ if failure is not None:
229
+ raise failure
230
+ self.__cached, self._refresh_at = token, now + ttl
231
+ return token
basaltic/client.py ADDED
@@ -0,0 +1,144 @@
1
+ """Generated service accessors; do not edit."""
2
+
3
+ from functools import cached_property
4
+
5
+ from ._client import AsyncClientBase, SyncClientBase
6
+ from .services.audit import AsyncAuditService, AuditService
7
+ from .services.billing import AsyncBillingService, BillingService
8
+ from .services.catalog import AsyncCatalogService, CatalogService
9
+ from .services.certificate import AsyncCertificateService, CertificateService
10
+ from .services.compute import AsyncComputeService, ComputeService
11
+ from .services.dns import AsyncDnsService, DnsService
12
+ from .services.iam import AsyncIamService, IamService
13
+ from .services.kms import AsyncKmsService, KmsService
14
+ from .services.loadbalancer import AsyncLoadbalancerService, LoadbalancerService
15
+ from .services.network import AsyncNetworkService, NetworkService
16
+ from .services.quota import AsyncQuotaService, QuotaService
17
+ from .services.secrets import AsyncSecretsService, SecretsService
18
+ from .services.storage import AsyncStorageService, StorageService
19
+ from .services.telemetry import AsyncTelemetryService, TelemetryService
20
+ from .services.workspace import AsyncWorkspaceService, WorkspaceService
21
+
22
+
23
+ class Client(SyncClientBase):
24
+ @cached_property
25
+ def audit(self) -> AuditService:
26
+ return AuditService(self._transport)
27
+
28
+ @cached_property
29
+ def billing(self) -> BillingService:
30
+ return BillingService(self._transport)
31
+
32
+ @cached_property
33
+ def catalog(self) -> CatalogService:
34
+ return CatalogService(self._transport)
35
+
36
+ @cached_property
37
+ def certificate(self) -> CertificateService:
38
+ return CertificateService(self._transport)
39
+
40
+ @cached_property
41
+ def compute(self) -> ComputeService:
42
+ return ComputeService(self._transport)
43
+
44
+ @cached_property
45
+ def dns(self) -> DnsService:
46
+ return DnsService(self._transport)
47
+
48
+ @cached_property
49
+ def iam(self) -> IamService:
50
+ return IamService(self._transport)
51
+
52
+ @cached_property
53
+ def kms(self) -> KmsService:
54
+ return KmsService(self._transport)
55
+
56
+ @cached_property
57
+ def loadbalancer(self) -> LoadbalancerService:
58
+ return LoadbalancerService(self._transport)
59
+
60
+ @cached_property
61
+ def network(self) -> NetworkService:
62
+ return NetworkService(self._transport)
63
+
64
+ @cached_property
65
+ def quota(self) -> QuotaService:
66
+ return QuotaService(self._transport)
67
+
68
+ @cached_property
69
+ def secrets(self) -> SecretsService:
70
+ return SecretsService(self._transport)
71
+
72
+ @cached_property
73
+ def storage(self) -> StorageService:
74
+ return StorageService(self._transport)
75
+
76
+ @cached_property
77
+ def telemetry(self) -> TelemetryService:
78
+ return TelemetryService(self._transport)
79
+
80
+ @cached_property
81
+ def workspace(self) -> WorkspaceService:
82
+ return WorkspaceService(self._transport)
83
+
84
+
85
+ class AsyncClient(AsyncClientBase):
86
+ @cached_property
87
+ def audit(self) -> AsyncAuditService:
88
+ return AsyncAuditService(self._transport)
89
+
90
+ @cached_property
91
+ def billing(self) -> AsyncBillingService:
92
+ return AsyncBillingService(self._transport)
93
+
94
+ @cached_property
95
+ def catalog(self) -> AsyncCatalogService:
96
+ return AsyncCatalogService(self._transport)
97
+
98
+ @cached_property
99
+ def certificate(self) -> AsyncCertificateService:
100
+ return AsyncCertificateService(self._transport)
101
+
102
+ @cached_property
103
+ def compute(self) -> AsyncComputeService:
104
+ return AsyncComputeService(self._transport)
105
+
106
+ @cached_property
107
+ def dns(self) -> AsyncDnsService:
108
+ return AsyncDnsService(self._transport)
109
+
110
+ @cached_property
111
+ def iam(self) -> AsyncIamService:
112
+ return AsyncIamService(self._transport)
113
+
114
+ @cached_property
115
+ def kms(self) -> AsyncKmsService:
116
+ return AsyncKmsService(self._transport)
117
+
118
+ @cached_property
119
+ def loadbalancer(self) -> AsyncLoadbalancerService:
120
+ return AsyncLoadbalancerService(self._transport)
121
+
122
+ @cached_property
123
+ def network(self) -> AsyncNetworkService:
124
+ return AsyncNetworkService(self._transport)
125
+
126
+ @cached_property
127
+ def quota(self) -> AsyncQuotaService:
128
+ return AsyncQuotaService(self._transport)
129
+
130
+ @cached_property
131
+ def secrets(self) -> AsyncSecretsService:
132
+ return AsyncSecretsService(self._transport)
133
+
134
+ @cached_property
135
+ def storage(self) -> AsyncStorageService:
136
+ return AsyncStorageService(self._transport)
137
+
138
+ @cached_property
139
+ def telemetry(self) -> AsyncTelemetryService:
140
+ return AsyncTelemetryService(self._transport)
141
+
142
+ @cached_property
143
+ def workspace(self) -> AsyncWorkspaceService:
144
+ return AsyncWorkspaceService(self._transport)
basaltic/config.py ADDED
@@ -0,0 +1,151 @@
1
+ """Explicit and environment-based configuration shared by all services."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import os
7
+ import re
8
+ from collections.abc import Mapping
9
+ from dataclasses import dataclass, field
10
+ from types import MappingProxyType
11
+ from typing import TYPE_CHECKING
12
+ from urllib.parse import urlsplit
13
+ from uuid import uuid4
14
+
15
+ if TYPE_CHECKING:
16
+ from .auth import AsyncTokenProvider, TokenProvider
17
+
18
+
19
+ def validate_url(value: str) -> str:
20
+ try:
21
+ u = urlsplit(value)
22
+ valid = u.scheme in ("http", "https") and u.hostname and u.port != 0
23
+ except ValueError:
24
+ valid = False
25
+ if (
26
+ not valid
27
+ or u.username
28
+ or u.password
29
+ or u.query
30
+ or u.fragment
31
+ or re.search(r"[\s\x00-\x1f\x7f]", value)
32
+ ):
33
+ raise ValueError(
34
+ "Endpoint must be an absolute HTTP(S) URL without credentials, query or fragment."
35
+ )
36
+ return value.rstrip("/")
37
+
38
+
39
+ def validate_token(value: str) -> str:
40
+ if (
41
+ not isinstance(value, str)
42
+ or not value
43
+ or not value.isascii()
44
+ or re.search(r"[\s\x00-\x1f\x7f]", value)
45
+ ):
46
+ raise ValueError("A nonempty bearer token without whitespace is required.")
47
+ return value
48
+
49
+
50
+ def positive_timeout(value: float) -> float:
51
+ if isinstance(value, bool) or not math.isfinite(value) or value <= 0:
52
+ raise ValueError("timeout must be a finite positive number of seconds.")
53
+ return value
54
+
55
+
56
+ def attempt_count(value: int) -> int:
57
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 10:
58
+ raise ValueError("max_attempts must be an integer between 1 and 10.")
59
+ return value
60
+
61
+
62
+ @dataclass(frozen=True, kw_only=True)
63
+ class RequestOptions:
64
+ account_id: str | None = None
65
+ idempotency_key: str | None = None
66
+ headers: Mapping[str, str] = field(default_factory=dict)
67
+ timeout: float | None = None
68
+ max_attempts: int | None = None
69
+
70
+
71
+ @dataclass(frozen=True, kw_only=True)
72
+ class Config:
73
+ access_key_id: str | None = field(default=None, repr=False)
74
+ secret_access_key: str | None = field(default=None, repr=False)
75
+ access_token: str | None = field(default=None, repr=False)
76
+ token_provider: TokenProvider | AsyncTokenProvider | None = field(default=None, repr=False)
77
+ anonymous: bool = False
78
+ region: str | None = None
79
+ account_id: str | None = None
80
+ domain: str | None = None
81
+ endpoints: Mapping[str, str] = field(default_factory=dict)
82
+ token_url: str | None = None
83
+ read_environment: bool = True
84
+ timeout: float = 30.0
85
+ max_attempts: int = 4
86
+ base_delay: float = 0.2
87
+ max_delay: float = 20.0
88
+
89
+ def __post_init__(self) -> None:
90
+ env = os.environ if self.read_environment else {}
91
+ for key in ("region", "account_id", "domain"):
92
+ if getattr(self, key) is None:
93
+ object.__setattr__(
94
+ self,
95
+ key,
96
+ env.get("BASALTIC_" + key.upper(), "basaltic.sh" if key == "domain" else ""),
97
+ )
98
+ if not re.fullmatch(
99
+ r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?",
100
+ self.domain or "",
101
+ ):
102
+ raise ValueError("Invalid API domain.")
103
+ endpoints = {
104
+ k.removeprefix("BASALTIC_ENDPOINT_URL_").lower(): v
105
+ for k, v in env.items()
106
+ if k.startswith("BASALTIC_ENDPOINT_URL_") and v
107
+ }
108
+ endpoints.update(self.endpoints)
109
+ endpoints = {key: validate_url(value) for key, value in endpoints.items()}
110
+ object.__setattr__(self, "endpoints", MappingProxyType(endpoints))
111
+ explicit_keys = self.access_key_id is not None or self.secret_access_key is not None
112
+ if self.access_token is None:
113
+ object.__setattr__(
114
+ self, "access_token", "" if explicit_keys else env.get("BASALTIC_ACCESS_TOKEN", "")
115
+ )
116
+ for key in ("access_key_id", "secret_access_key"):
117
+ if getattr(self, key) is None:
118
+ object.__setattr__(self, key, env.get("BASALTIC_" + key.upper(), ""))
119
+ if (
120
+ not self.anonymous
121
+ and not self.token_provider
122
+ and not self.access_token
123
+ and not (self.access_key_id and self.secret_access_key)
124
+ ):
125
+ raise ValueError(
126
+ "Configure an access key pair, bearer token, token provider or explicit anonymous access."
127
+ )
128
+ if self.access_token:
129
+ validate_token(self.access_token)
130
+ positive_timeout(self.timeout)
131
+ attempt_count(self.max_attempts)
132
+ if any(not math.isfinite(v) or v < 0 for v in (self.base_delay, self.max_delay)):
133
+ raise ValueError("Retry delays must be finite and nonnegative.")
134
+
135
+ def endpoint(self, service: str, template: str) -> str:
136
+ if service in self.endpoints:
137
+ return self.endpoints[service]
138
+ if "{region}" in template and not re.fullmatch(
139
+ r"[a-z0-9]+(?:-[a-z0-9]+)*", self.region or ""
140
+ ):
141
+ raise ValueError(f"A valid region is required for {service}.")
142
+ return validate_url(
143
+ template.replace("basaltic.sh", self.domain or "").replace(
144
+ "{region}", self.region or ""
145
+ )
146
+ )
147
+
148
+
149
+ def new_idempotency_key() -> str:
150
+ """Create once per logical mutation and reuse when retrying that mutation."""
151
+ return str(uuid4())
basaltic/errors.py ADDED
@@ -0,0 +1,73 @@
1
+ """Structured SDK errors; transport and OAuth failures never retain raw requests."""
2
+
3
+ import re
4
+
5
+
6
+ class BasalticError(Exception):
7
+ """Base class for SDK failures."""
8
+
9
+
10
+ class ProtocolError(BasalticError):
11
+ pass
12
+
13
+
14
+ class TransportError(BasalticError):
15
+ pass
16
+
17
+
18
+ class RequestTimeoutError(TransportError):
19
+ pass
20
+
21
+
22
+ class AuthenticationError(BasalticError):
23
+ def __init__(self, status_code: int = 0, error_code: str = "") -> None:
24
+ self.status_code = status_code
25
+ self.error_code = error_code
26
+ super().__init__(f"Token exchange failed (HTTP {status_code}, code {error_code}).")
27
+
28
+
29
+ class AmbiguousReferenceError(BasalticError):
30
+ def __init__(self) -> None:
31
+ super().__init__("Reference matches multiple resources; narrow its parent scope.")
32
+
33
+
34
+ class ApiError(BasalticError):
35
+ def __init__(
36
+ self,
37
+ status_code: int,
38
+ error_code: str,
39
+ message: str,
40
+ request_id: str,
41
+ operation_id: str,
42
+ headers: dict[str, str] | None = None,
43
+ ) -> None:
44
+ self.status_code = status_code
45
+ self.error_code = error_code
46
+ self.request_id = request_id
47
+ self.operation_id = operation_id
48
+ self.headers = dict(headers or {})
49
+ super().__init__(f"{operation_id}: {message} (HTTP {status_code}, request {request_id})")
50
+
51
+ def is_not_found(self) -> bool:
52
+ return self.status_code in (404, 410)
53
+
54
+ def is_unauthorized(self) -> bool:
55
+ return self.status_code == 401
56
+
57
+ def is_quota_exceeded(self) -> bool:
58
+ return bool(re.search(r"(?:^|_)(?:QUOTA|LIMIT)_EXCEEDED$", self.error_code))
59
+
60
+ def is_access_denied(self) -> bool:
61
+ return self.status_code == 403 and not self.is_quota_exceeded()
62
+
63
+ def is_conflict(self) -> bool:
64
+ return self.status_code == 409
65
+
66
+ def is_invalid_input(self) -> bool:
67
+ return self.status_code in (400, 422)
68
+
69
+ def is_rate_limited(self) -> bool:
70
+ return self.status_code == 429
71
+
72
+ def is_transient(self) -> bool:
73
+ return self.status_code == 429 or self.status_code >= 500
@@ -0,0 +1,17 @@
1
+ """Service-specific typed request and response models."""
2
+
3
+ from . import audit as audit
4
+ from . import billing as billing
5
+ from . import catalog as catalog
6
+ from . import certificate as certificate
7
+ from . import compute as compute
8
+ from . import dns as dns
9
+ from . import iam as iam
10
+ from . import kms as kms
11
+ from . import loadbalancer as loadbalancer
12
+ from . import network as network
13
+ from . import quota as quota
14
+ from . import secrets as secrets
15
+ from . import storage as storage
16
+ from . import telemetry as telemetry
17
+ from . import workspace as workspace