sharkmagic 0.1.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.
sharkmagic/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """Shark SDK for Python (docs/spec/v1/sdk.md). Standard library only.
2
+
3
+ from sharkmagic import Shark
4
+ shark = Shark() # SHARK_API_KEY, SHARK_API
5
+ space = shark.spaces.create(name="demo")
6
+ print(space.run("echo hi").stdout)
7
+ """
8
+
9
+ from ._errors import (AuthenticationError, CapacityError, CommandFailedError, CommandTimeoutError, ConflictError,
10
+ CreditsExhaustedError, DurableUnavailableError, IdempotencyMismatchError, InfraError,
11
+ InvalidRequestError, LeaseCapError, NotFoundError, OpExpiredError, PermissionDeniedError,
12
+ QuotaExceededError, RateLimitError, ServerError, SharkError, SpaceLimitError, UnsupportedError,
13
+ WaitTimeoutError)
14
+ from ._errors import ConnectionError
15
+ from ._transport import ROUTES, VERSION, Event, Transport
16
+ from .client import (AccountPorts, AccountSecrets, AsyncShark, Egress, ExecResult, FileEntry, Op, Ports, Shark, Space, SpaceDefaults,
17
+ SpaceEgress, SpaceSecrets, new_op_id)
18
+
19
+ __version__ = VERSION
20
+ __all__ = ["Shark", "AsyncShark", "Space", "SpaceSecrets", "SpaceEgress", "Egress", "AccountSecrets", "SpaceDefaults", "Ports", "AccountPorts", "Op", "ExecResult", "FileEntry", "Event", "Transport", "ROUTES", "new_op_id",
21
+ "SharkError", "AuthenticationError", "PermissionDeniedError", "NotFoundError", "ConflictError",
22
+ "IdempotencyMismatchError", "OpExpiredError", "InvalidRequestError", "UnsupportedError", "RateLimitError",
23
+ "LeaseCapError", "SpaceLimitError", "QuotaExceededError", "CreditsExhaustedError", "ServerError",
24
+ "CapacityError", "DurableUnavailableError", "CommandFailedError", "InfraError", "CommandTimeoutError",
25
+ "WaitTimeoutError", "ConnectionError"]
sharkmagic/_errors.py ADDED
@@ -0,0 +1,172 @@
1
+ """Typed errors (docs/spec/v1/sdk.md §5): one class per error.code family."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+
8
+ class SharkError(Exception):
9
+ """Base of every SDK error. ``code`` is the API's stable error code."""
10
+
11
+ def __init__(self, message: str, *, code: str = "", status: int = 0, request_id: str = "",
12
+ details: Optional[dict] = None, retryable: bool = False, retry_after: Optional[float] = None):
13
+ super().__init__(message)
14
+ self.message = message
15
+ self.code = code
16
+ self.status = status
17
+ self.request_id = request_id
18
+ self.details = details or {}
19
+ self.retryable = retryable
20
+ self.retry_after = retry_after
21
+
22
+ def __repr__(self) -> str:
23
+ return f"{type(self).__name__}(code={self.code!r}, status={self.status}, message={self.message!r})"
24
+
25
+
26
+ class AuthenticationError(SharkError):
27
+ pass
28
+
29
+
30
+ class PermissionDeniedError(SharkError):
31
+ pass
32
+
33
+
34
+ class NotFoundError(SharkError):
35
+ pass
36
+
37
+
38
+ class ConflictError(SharkError):
39
+ pass
40
+
41
+
42
+ class IdempotencyMismatchError(ConflictError):
43
+ pass
44
+
45
+
46
+ class OpExpiredError(SharkError):
47
+ pass
48
+
49
+
50
+ class InvalidRequestError(SharkError):
51
+ pass
52
+
53
+
54
+ class UnsupportedError(SharkError):
55
+ pass
56
+
57
+
58
+ class RateLimitError(SharkError):
59
+ pass
60
+
61
+
62
+ class LeaseCapError(RateLimitError):
63
+ @property
64
+ def scope(self) -> str:
65
+ return str(self.details.get("scope", ""))
66
+
67
+
68
+ class SpaceLimitError(RateLimitError):
69
+ pass
70
+
71
+
72
+ class QuotaExceededError(SharkError):
73
+ pass
74
+
75
+
76
+ class CreditsExhaustedError(SharkError):
77
+ pass
78
+
79
+
80
+ class ServerError(SharkError):
81
+ pass
82
+
83
+
84
+ class CapacityError(ServerError):
85
+ pass
86
+
87
+
88
+ class DurableUnavailableError(ServerError):
89
+ pass
90
+
91
+
92
+ class ConnectionError(SharkError): # noqa: A001 - the SDK's own, raised after retries
93
+ pass
94
+
95
+
96
+ class CommandFailedError(SharkError):
97
+ """Raised by run(..., check=True) when the command exits non-zero."""
98
+
99
+ def __init__(self, result: Any):
100
+ super().__init__(f"command exited {result.exit}", code="command_failed")
101
+ self.result = result
102
+ self.exit = result.exit
103
+ self.stdout = result.stdout
104
+ self.stderr = result.stderr
105
+ self.op_id = result.op_id
106
+
107
+
108
+ class InfraError(SharkError):
109
+ pass
110
+
111
+
112
+ class CommandTimeoutError(SharkError):
113
+ pass
114
+
115
+
116
+ class WaitTimeoutError(SharkError):
117
+ """op.wait(timeout=...) ran out of time (code ``wait_timeout``). The command keeps running: wait again,
118
+ ``op.refresh()``, or ``op.signal()`` it. ``.op_id``, ``.timeout`` (seconds)."""
119
+
120
+ def __init__(self, op_id: str, timeout: float):
121
+ super().__init__(f"op {op_id} still running after {timeout:g} s", code="wait_timeout",
122
+ details={"op_id": op_id, "timeout_s": timeout})
123
+ self.op_id = op_id
124
+ self.timeout = timeout
125
+
126
+
127
+ # code -> class (sdk.md §5)
128
+ CODE_CLASSES = {
129
+ "unauthenticated": AuthenticationError,
130
+ "auth_rate_limited": AuthenticationError,
131
+ "insufficient_scope": PermissionDeniedError,
132
+ "plan_restriction": PermissionDeniedError,
133
+ "tenant_not_allowed": PermissionDeniedError,
134
+ "not_found": NotFoundError,
135
+ "conflict": ConflictError,
136
+ "idempotency_mismatch": IdempotencyMismatchError,
137
+ "idempotency_in_progress": IdempotencyMismatchError,
138
+ "op_expired": OpExpiredError,
139
+ "invalid_request": InvalidRequestError,
140
+ "invalid_op_id": InvalidRequestError,
141
+ "path_outside_space": InvalidRequestError,
142
+ "payload_too_large": InvalidRequestError,
143
+ "precondition_failed": ConflictError,
144
+ "upgrade_required": InvalidRequestError,
145
+ "unsupported": UnsupportedError,
146
+ "rate_limited": RateLimitError,
147
+ "lease_cap_reached": LeaseCapError,
148
+ "space_limit_reached": SpaceLimitError,
149
+ "quota_exceeded": QuotaExceededError,
150
+ "credits_exhausted": CreditsExhaustedError,
151
+ "internal": ServerError,
152
+ "capacity_unavailable": CapacityError,
153
+ "out_of_capacity": CapacityError,
154
+ "insufficient_storage": QuotaExceededError,
155
+ "canceled": CapacityError,
156
+ "durable_unavailable": DurableUnavailableError,
157
+ }
158
+
159
+ # HTTP status -> class, for codes this SDK does not know yet
160
+ STATUS_CLASSES = {400: InvalidRequestError, 401: AuthenticationError, 402: CreditsExhaustedError, 403: PermissionDeniedError,
161
+ 404: NotFoundError, 409: ConflictError, 410: OpExpiredError, 412: ConflictError, 413: InvalidRequestError,
162
+ 422: UnsupportedError, 429: RateLimitError}
163
+
164
+
165
+ def error_class(code: str, status: int) -> type:
166
+ if code in CODE_CLASSES:
167
+ return CODE_CLASSES[code]
168
+ if status in STATUS_CLASSES:
169
+ return STATUS_CLASSES[status]
170
+ if status >= 500:
171
+ return ServerError
172
+ return SharkError