ase-sdk-python 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.
ase_sdk/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """ASE Python SDK: AIPAAS and AIAAS HTTP / WebSocket clients."""
2
+
3
+ from .auth import build_aiaas_headers, build_signed_url
4
+ from .client import Client
5
+ from .exceptions import ASEError, ClientClosedError, HTTPError, ProtocolError, TransportError
6
+ from .models import (
7
+ AIAASRequest, AIPAASRequest, AIaaSRequest, AudioPayload, DataStatus,
8
+ ImagePayload, Request, RequestHeader, StatusContinue, StatusFirstFrame,
9
+ StatusForOnce, StatusLastFrame, TextPayload,
10
+ )
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ __all__ = [
15
+ "Client", "Request", "AIPAASRequest", "AIaaSRequest", "AIAASRequest",
16
+ "RequestHeader", "TextPayload", "AudioPayload", "ImagePayload", "DataStatus",
17
+ "StatusFirstFrame", "StatusContinue", "StatusLastFrame", "StatusForOnce",
18
+ "build_signed_url", "build_aiaas_headers", "ASEError", "HTTPError",
19
+ "TransportError", "ProtocolError", "ClientClosedError", "__version__",
20
+ ]
ase_sdk/auth.py ADDED
@@ -0,0 +1,167 @@
1
+ """AIPAAS URL signing and AIAAS HTTP body/header signing.
2
+
3
+ These functions never perform I/O. Sign AIAAS requests after serialization and
4
+ send the exact same bytes: even JSON whitespace changes the required digest.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import hashlib
11
+ import hmac
12
+ import re
13
+ from datetime import datetime, timezone
14
+ from email.utils import format_datetime, parsedate_to_datetime
15
+ from urllib.parse import SplitResult, urlencode, urlsplit, urlunsplit
16
+
17
+ __all__ = ["build_signed_url", "build_aiaas_headers", "hmac_sha256"]
18
+
19
+ _DATE_PATTERN = re.compile(
20
+ r"[A-Z][a-z]{2}, [0-9]{2} [A-Z][a-z]{2} [0-9]{4} "
21
+ r"[0-9]{2}:[0-9]{2}:[0-9]{2} (?:UTC|GMT|\+0000)"
22
+ )
23
+
24
+
25
+ def _require_text(value: str, name: str) -> None:
26
+ if not isinstance(value, str):
27
+ raise TypeError(f"{name} must be a string")
28
+ if not value or not value.strip():
29
+ raise ValueError(f"{name} must not be empty")
30
+
31
+
32
+ def _validate_credentials(api_key: str, api_secret: str) -> None:
33
+ _require_text(api_key, "api_key")
34
+ _require_text(api_secret, "api_secret")
35
+ # api_key is embedded in a quoted Authorization parameter. Reject header
36
+ # delimiters instead of changing the credential by silently escaping it.
37
+ if any(ord(char) < 32 or ord(char) > 126 or char in '\\"' for char in api_key):
38
+ raise ValueError("api_key must contain printable ASCII without quotes or backslashes")
39
+
40
+
41
+ def _parse_endpoint(endpoint: str) -> SplitResult:
42
+ _require_text(endpoint, "endpoint")
43
+ if not endpoint.isascii():
44
+ raise ValueError("endpoint must be ASCII; encode the hostname with IDNA and percent-encode the path")
45
+ if any(char.isspace() or ord(char) < 32 or ord(char) == 127 for char in endpoint):
46
+ raise ValueError("endpoint must not contain whitespace or control characters")
47
+ try:
48
+ parts = urlsplit(endpoint)
49
+ # Accessing port also validates malformed or out-of-range port numbers.
50
+ parts.port
51
+ except ValueError:
52
+ raise ValueError("endpoint must be a valid absolute URL") from None
53
+ if parts.scheme not in {"http", "https", "ws", "wss"} or not parts.hostname:
54
+ raise ValueError("endpoint must be an absolute HTTP(S) or WS(S) URL")
55
+ if parts.username is not None or parts.password is not None or "?" in endpoint or "#" in endpoint:
56
+ raise ValueError("endpoint must not contain credentials, a query or a fragment")
57
+ return parts
58
+
59
+
60
+ def _format_date(date: datetime | str | None) -> str:
61
+ if date is None:
62
+ date = datetime.now(timezone.utc)
63
+ if isinstance(date, str):
64
+ if not _DATE_PATTERN.fullmatch(date):
65
+ raise ValueError("date must be an RFC 1123 date in UTC")
66
+ try:
67
+ parsed = parsedate_to_datetime(date)
68
+ except (ValueError, TypeError, OverflowError):
69
+ raise ValueError("date must be an RFC 1123 date in UTC") from None
70
+ if parsed.tzinfo is None or parsed.utcoffset().total_seconds() != 0:
71
+ raise ValueError("date must be an RFC 1123 date in UTC")
72
+ return date
73
+ if not isinstance(date, datetime):
74
+ raise TypeError("date must be a timezone-aware datetime or an RFC 1123 string")
75
+ if date.tzinfo is None or date.utcoffset() is None:
76
+ raise ValueError("date must be timezone-aware")
77
+ # Go time.Now().UTC().Format(time.RFC1123) uses "UTC", not "+0000".
78
+ return format_datetime(date.astimezone(timezone.utc)).replace("+0000", "UTC")
79
+
80
+
81
+ def hmac_sha256(data: bytes, secret: str) -> str:
82
+ """Return standard Base64 of HMAC-SHA256 over *data* using a UTF-8 secret."""
83
+ if not isinstance(data, bytes):
84
+ raise TypeError("data must be bytes")
85
+ if not isinstance(secret, str):
86
+ raise TypeError("secret must be a string")
87
+ if not secret:
88
+ raise ValueError("secret must not be empty")
89
+ return base64.b64encode(hmac.new(secret.encode("utf-8"), data, hashlib.sha256).digest()).decode("ascii")
90
+
91
+
92
+ def build_signed_url(
93
+ endpoint: str,
94
+ method: str,
95
+ api_key: str,
96
+ api_secret: str,
97
+ *,
98
+ date: datetime | str | None = None,
99
+ ) -> str:
100
+ """Sign an AIPAAS HTTP POST or WebSocket GET endpoint.
101
+
102
+ AIAAS WebSocket connections also use this URL authentication scheme. The
103
+ returned URL contains temporary authentication material and should not be
104
+ logged. The endpoint must not already contain a query or fragment.
105
+ """
106
+ parts = _parse_endpoint(endpoint)
107
+ _validate_credentials(api_key, api_secret)
108
+ _require_text(method, "method")
109
+ method = method.upper()
110
+ if method not in {"GET", "POST"}:
111
+ raise ValueError("method must be GET or POST")
112
+ request_date = _format_date(date)
113
+ request_path = parts.path or "/"
114
+ signature_data = (
115
+ f"host: {parts.netloc}\ndate: {request_date}\n"
116
+ f"{method} {request_path} HTTP/1.1"
117
+ )
118
+ signature = hmac_sha256(signature_data.encode("utf-8"), api_secret)
119
+ authorization = (
120
+ f'api_key="{api_key}", algorithm="hmac-sha256", '
121
+ f'headers="host date request-line", signature="{signature}"'
122
+ )
123
+ query = [
124
+ ("authorization", base64.b64encode(authorization.encode("utf-8")).decode("ascii")),
125
+ ("date", request_date),
126
+ ("host", parts.netloc),
127
+ ]
128
+ return urlunsplit(parts._replace(path=request_path, query=urlencode(query)))
129
+
130
+
131
+ def build_aiaas_headers(
132
+ endpoint: str,
133
+ body: bytes,
134
+ api_key: str,
135
+ api_secret: str,
136
+ *,
137
+ date: datetime | str | None = None,
138
+ ) -> dict[str, str]:
139
+ """Create AIAAS HTTP POST headers, signing the exact outgoing body bytes.
140
+
141
+ Unlike AIPAAS URL signing, this scheme includes a SHA-256 body digest and
142
+ sends the unencoded ``hmac ...`` authorization value as an HTTP header.
143
+ """
144
+ parts = _parse_endpoint(endpoint)
145
+ if parts.scheme not in {"http", "https"}:
146
+ raise ValueError("AIAAS body signing requires an HTTP(S) endpoint")
147
+ _validate_credentials(api_key, api_secret)
148
+ if not isinstance(body, bytes):
149
+ raise TypeError("body must be bytes; serialize once and send the same bytes")
150
+ request_date = _format_date(date)
151
+ digest = "SHA-256=" + base64.b64encode(hashlib.sha256(body).digest()).decode("ascii")
152
+ request_path = parts.path or "/"
153
+ signature_data = (
154
+ f"host: {parts.netloc}\ndate: {request_date}\n"
155
+ f"POST {request_path} HTTP/1.1\ndigest: {digest}"
156
+ )
157
+ signature = hmac_sha256(signature_data.encode("utf-8"), api_secret)
158
+ return {
159
+ "Content-Type": "application/json",
160
+ "Host": parts.netloc,
161
+ "Date": request_date,
162
+ "Digest": digest,
163
+ "Authorization": (
164
+ f'hmac api_key="{api_key}", algorithm="hmac-sha256", '
165
+ f'headers="host date request-line digest", signature="{signature}"'
166
+ ),
167
+ }
ase_sdk/client.py ADDED
@@ -0,0 +1,346 @@
1
+ """Synchronous HTTP and full-duplex WebSocket client for ASE."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import socket
8
+ import threading
9
+ import time
10
+ from collections.abc import Mapping
11
+ from typing import Any, Literal
12
+ from urllib.parse import urlsplit, urlunsplit
13
+
14
+ import requests
15
+ from websockets.exceptions import WebSocketException
16
+ from websockets.sync.client import ClientConnection, connect as ws_connect
17
+
18
+ from .auth import build_aiaas_headers, build_signed_url
19
+ from .exceptions import ClientClosedError, HTTPError, ProtocolError, TransportError
20
+ from .models import AIaaSRequest, DataStatus, Request, to_jsonable
21
+
22
+ Mode = Literal["aipaas", "aiaas"]
23
+ RequestLike = Request | AIaaSRequest | Mapping[str, Any]
24
+
25
+
26
+ class _NoNetrcAuth(requests.auth.AuthBase):
27
+ """Keep ASE signatures intact while retaining environment proxy support."""
28
+
29
+ def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
30
+ return request
31
+
32
+
33
+ def _timeout(name: str, value: float | None) -> float | None:
34
+ if value is not None and (
35
+ isinstance(value, bool) or not isinstance(value, (int, float))
36
+ or not math.isfinite(value) or value <= 0
37
+ ):
38
+ raise ValueError(f"{name} must be positive or None")
39
+ return value
40
+
41
+
42
+ class _SocketDeadline:
43
+ """Interrupt a blocked socket write without altering the receive timeout."""
44
+
45
+ def __init__(self, connection: ClientConnection, timeout: float | None) -> None:
46
+ self._connection = connection
47
+ self._timeout = timeout
48
+ self._lock = threading.Lock()
49
+ self._active = False
50
+ self._timer: threading.Timer | None = None
51
+ self.expired = False
52
+
53
+ def _expire(self) -> None:
54
+ # Serializing completion with shutdown guarantees an old timer cannot
55
+ # interrupt the next frame after a successful send has returned.
56
+ with self._lock:
57
+ if not self._active:
58
+ return
59
+ self.expired = True
60
+ try:
61
+ # connection.close() needs websockets' protocol lock, which a
62
+ # blocked sendall holds. shutdown doesn't need that lock and
63
+ # wakes both sendall and the library's receiving thread.
64
+ self._connection.socket.shutdown(socket.SHUT_RDWR)
65
+ except OSError:
66
+ pass # Another timeout or close already shut down the socket.
67
+
68
+ def __enter__(self) -> _SocketDeadline:
69
+ self._active = True
70
+ if self._timeout is not None:
71
+ self._timer = threading.Timer(self._timeout, self._expire)
72
+ self._timer.name = "ase-sdk-websocket-deadline"
73
+ self._timer.daemon = True
74
+ self._timer.start()
75
+ return self
76
+
77
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
78
+ with self._lock:
79
+ self._active = False
80
+ if self._timer is not None:
81
+ self._timer.cancel()
82
+ self._timer.join()
83
+
84
+
85
+ class Client:
86
+ """An application client and one lazily opened WebSocket session.
87
+
88
+ A single sender and receiver may operate concurrently. HTTP requests are
89
+ serialized per client. There are no automatic reconnects or POST retries
90
+ unless ``retries`` is explicitly set. Exiting a context closes resources.
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ app_id: str,
96
+ api_key: str,
97
+ api_secret: str,
98
+ host: str,
99
+ uri: str,
100
+ *,
101
+ mode: Mode = "aipaas",
102
+ tls: bool = True,
103
+ timeout: float | None = 30,
104
+ retries: int = 0,
105
+ retry_backoff: float = 0.5,
106
+ handshake_timeout: float | None = 10,
107
+ read_timeout: float | None = 30,
108
+ write_timeout: float | None = 30,
109
+ close_timeout: float | None = 5,
110
+ max_size: int | None = 16 * 1024 * 1024,
111
+ connect_headers: Mapping[str, str] | None = None,
112
+ ) -> None:
113
+ for name, value in (("app_id", app_id), ("api_key", api_key), ("api_secret", api_secret)):
114
+ if not isinstance(value, str) or not value.strip():
115
+ raise ValueError(f"{name} must be a nonempty string")
116
+ if mode not in ("aipaas", "aiaas"):
117
+ raise ValueError("mode must be 'aipaas' or 'aiaas'")
118
+ if not isinstance(tls, bool):
119
+ raise ValueError("tls must be a bool")
120
+ if not isinstance(host, str) or not host or any(char in host for char in "/?#@"):
121
+ raise ValueError("host must be a hostname with optional port, without a scheme or path")
122
+ if not isinstance(uri, str) or not uri.startswith("/") or uri.startswith("//") or "?" in uri or "#" in uri:
123
+ raise ValueError("uri must be an absolute path without query parameters or fragments")
124
+ if not (host + uri).isascii() or any(ord(char) <= 32 or ord(char) == 127 for char in host + uri):
125
+ raise ValueError("host and uri must be ASCII without whitespace; percent-encode the path")
126
+ if isinstance(retries, bool) or not isinstance(retries, int) or retries < 0:
127
+ raise ValueError("retries must be a nonnegative integer")
128
+ if isinstance(retry_backoff, bool) or not isinstance(retry_backoff, (int, float)) or not math.isfinite(retry_backoff) or retry_backoff < 0:
129
+ raise ValueError("retry_backoff must be nonnegative")
130
+ if max_size is not None and (isinstance(max_size, bool) or not isinstance(max_size, int) or max_size <= 0):
131
+ raise ValueError("max_size must be a positive integer or None")
132
+
133
+ endpoint = f"{'https' if tls else 'http'}://{host}{uri}"
134
+ # requests normalizes hostnames and percent-encoded unreserved path
135
+ # characters. Sign that exact URL, so the bytes on the wire match.
136
+ try:
137
+ prepared = requests.Request("POST", endpoint).prepare()
138
+ normalized = urlsplit(prepared.url or "")
139
+ if not normalized.hostname or normalized.username or normalized.password:
140
+ raise ValueError
141
+ normalized.port # Validate the port before a network call.
142
+ except (ValueError, requests.RequestException):
143
+ raise ValueError("invalid host or uri") from None
144
+ self._http_endpoint = urlunsplit(normalized)
145
+ self._ws_endpoint = urlunsplit(normalized._replace(scheme="wss" if tls else "ws"))
146
+ self.app_id = app_id
147
+ self.mode = mode
148
+ self._api_key = api_key
149
+ self._api_secret = api_secret
150
+ self._timeout = _timeout("timeout", timeout)
151
+ self._handshake_timeout = _timeout("handshake_timeout", handshake_timeout)
152
+ self._read_timeout = _timeout("read_timeout", read_timeout)
153
+ self._write_timeout = _timeout("write_timeout", write_timeout)
154
+ self._close_timeout = _timeout("close_timeout", close_timeout)
155
+ self._retries = retries
156
+ self._retry_backoff = retry_backoff
157
+ self._max_size = max_size
158
+ self._connect_headers = dict(connect_headers or {})
159
+ reserved = {"authorization", "digest", "date", "host", "connection", "upgrade", "content-length"}
160
+ for key, value in self._connect_headers.items():
161
+ if not isinstance(key, str) or not isinstance(value, str):
162
+ raise ValueError("connect_headers must contain string keys and values")
163
+ if key.lower() in reserved or key.lower().startswith("sec-websocket-"):
164
+ raise ValueError("connect_headers cannot override authentication or WebSocket handshake fields")
165
+ if "\r" in key + value or "\n" in key + value:
166
+ raise ValueError("connect_headers cannot contain line breaks")
167
+ self._http = requests.Session()
168
+ self._http.auth = _NoNetrcAuth()
169
+ self._connection: ClientConnection | None = None
170
+ self._closed = False
171
+ self._connection_lock = threading.Lock()
172
+ self._http_lock = threading.Lock()
173
+ self._send_lock = threading.Lock()
174
+
175
+ def _ensure_open(self) -> None:
176
+ if self._closed:
177
+ raise ClientClosedError("ASE client is closed")
178
+
179
+ def _serialize(self, request: RequestLike, mode: Mode, *, once: bool) -> bytes:
180
+ if isinstance(request, Request):
181
+ if mode != "aipaas":
182
+ raise ValueError("Request is an AIPAAS envelope; use AIaaSRequest for AIAAS")
183
+ value = request.to_dict()
184
+ elif isinstance(request, AIaaSRequest):
185
+ if mode != "aiaas":
186
+ raise ValueError("AIaaSRequest requires AIAAS mode or an explicit aiaas method")
187
+ value = request.to_dict()
188
+ elif isinstance(request, Mapping):
189
+ value = to_jsonable(request)
190
+ else:
191
+ raise TypeError("request must be a Request, AIaaSRequest, or mapping")
192
+ sections = ("header", "parameter", "payload") if mode == "aipaas" else ("common", "business", "data")
193
+ wrong_sections = ("common", "business", "data") if mode == "aipaas" else ("header", "parameter", "payload")
194
+ if any(key in value for key in wrong_sections):
195
+ raise ValueError("request envelope does not match the selected mode")
196
+ for key in sections:
197
+ if key in value and not isinstance(value[key], dict):
198
+ raise ValueError(f"request section {key} must be a mapping")
199
+ header = value.setdefault(sections[0], {})
200
+ if "app_id" in header and header["app_id"] != self.app_id:
201
+ raise ValueError("request app_id must match the client's app_id")
202
+ header.setdefault("app_id", self.app_id)
203
+ if once and mode == "aipaas":
204
+ header.setdefault("status", int(DataStatus.ONCE))
205
+ try:
206
+ return json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":")).encode("utf-8")
207
+ except (TypeError, ValueError, UnicodeError):
208
+ raise ValueError("request must contain valid JSON values; encode media bytes explicitly") from None
209
+
210
+ def _post(self, request: RequestLike, mode: Mode) -> bytes:
211
+ self._ensure_open()
212
+ body = self._serialize(request, mode, once=True)
213
+ with self._http_lock:
214
+ for attempt in range(self._retries + 1):
215
+ self._ensure_open()
216
+ if mode == "aiaas":
217
+ endpoint = self._http_endpoint
218
+ headers = build_aiaas_headers(endpoint, body, self._api_key, self._api_secret)
219
+ else:
220
+ endpoint = build_signed_url(self._http_endpoint, "POST", self._api_key, self._api_secret)
221
+ headers = {"Content-Type": "application/json"}
222
+ try:
223
+ response = self._http.post(endpoint, data=body, headers=headers, timeout=self._timeout, allow_redirects=False)
224
+ try:
225
+ status, result = response.status_code, response.content
226
+ finally:
227
+ response.close()
228
+ except requests.RequestException:
229
+ if attempt == self._retries:
230
+ raise TransportError("ASE HTTP connection failed or timed out") from None
231
+ else:
232
+ if status == 200:
233
+ return result
234
+ if attempt == self._retries or (status != 429 and not 500 <= status < 600):
235
+ raise HTTPError(status, result)
236
+ time.sleep(self._retry_backoff * (2 ** attempt))
237
+ raise AssertionError("unreachable")
238
+
239
+ @staticmethod
240
+ def _decode(raw: bytes) -> dict[str, Any]:
241
+ try:
242
+ result = json.loads(raw)
243
+ except (ValueError, UnicodeError):
244
+ raise ProtocolError("ASE response is not valid JSON; use the raw method to inspect it") from None
245
+ if not isinstance(result, dict):
246
+ raise ProtocolError("ASE response must be a JSON object")
247
+ return result
248
+
249
+ def once_raw(self, request: RequestLike) -> bytes:
250
+ """POST one request in the configured mode and return response bytes."""
251
+ return self._post(request, self.mode)
252
+
253
+ def once(self, request: RequestLike) -> dict[str, Any]:
254
+ """POST once and decode JSON. Business error codes remain in the result."""
255
+ return self._decode(self.once_raw(request))
256
+
257
+ def once_aiaas_raw(self, request: RequestLike) -> bytes:
258
+ return self._post(request, "aiaas")
259
+
260
+ def once_aiaas(self, request: RequestLike) -> dict[str, Any]:
261
+ return self._decode(self.once_aiaas_raw(request))
262
+
263
+ def _get_connection(self) -> ClientConnection:
264
+ with self._connection_lock:
265
+ self._ensure_open()
266
+ if self._connection is None:
267
+ # SendAIaaS in the Go SDK shares the same signed GET handshake.
268
+ endpoint = build_signed_url(self._ws_endpoint, "GET", self._api_key, self._api_secret)
269
+ try:
270
+ connection = ws_connect(
271
+ endpoint, additional_headers=self._connect_headers,
272
+ open_timeout=self._handshake_timeout, close_timeout=self._close_timeout,
273
+ max_size=self._max_size, compression=None, proxy=None,
274
+ )
275
+ # ClientConnection is its own context manager. Enter it
276
+ # explicitly for this client's lifetime (required by the
277
+ # non-legacy lifecycle in websockets 17 and later).
278
+ self._connection = connection.__enter__()
279
+ except (OSError, TimeoutError, WebSocketException, ValueError):
280
+ raise TransportError("ASE WebSocket handshake failed or timed out") from None
281
+ return self._connection
282
+
283
+ def connect(self) -> None:
284
+ """Open the WebSocket connection; send/receive also connect lazily."""
285
+ self._get_connection()
286
+
287
+ def _send(self, request: RequestLike, mode: Mode) -> None:
288
+ self._ensure_open()
289
+ body = self._serialize(request, mode, once=False).decode("utf-8")
290
+ with self._send_lock:
291
+ connection = self._get_connection()
292
+ deadline = _SocketDeadline(connection, self._write_timeout)
293
+ try:
294
+ with deadline:
295
+ connection.send(body)
296
+ except (OSError, TimeoutError, WebSocketException):
297
+ if deadline.expired:
298
+ raise TransportError("ASE WebSocket send timed out; the connection was closed") from None
299
+ raise TransportError("ASE WebSocket send failed") from None
300
+ if deadline.expired:
301
+ raise TransportError("ASE WebSocket send timed out; the connection was closed")
302
+
303
+ def send(self, request: RequestLike) -> None:
304
+ """Send one JSON frame; a write timeout closes the WebSocket session."""
305
+ self._send(request, self.mode)
306
+
307
+ def send_aiaas(self, request: RequestLike) -> None:
308
+ self._send(request, "aiaas")
309
+
310
+ def receive_raw(self) -> bytes:
311
+ """Receive one frame, with a timeout; usable concurrently with send."""
312
+ connection = self._get_connection()
313
+ try:
314
+ message = connection.recv(timeout=self._read_timeout)
315
+ except TimeoutError:
316
+ raise TransportError("ASE WebSocket receive timed out") from None
317
+ except (OSError, WebSocketException):
318
+ raise TransportError("ASE WebSocket receive failed or the connection closed") from None
319
+ return message if isinstance(message, bytes) else message.encode("utf-8")
320
+
321
+ def receive(self) -> dict[str, Any]:
322
+ return self._decode(self.receive_raw())
323
+
324
+ def close(self) -> None:
325
+ """Close HTTP resources and the WebSocket session. Safe to repeat."""
326
+ with self._connection_lock:
327
+ if self._closed:
328
+ return
329
+ self._closed = True
330
+ connection, self._connection = self._connection, None
331
+ try:
332
+ if connection is not None:
333
+ with _SocketDeadline(connection, self._close_timeout):
334
+ connection.__exit__(None, None, None)
335
+ finally:
336
+ self._http.close()
337
+
338
+ def destroy(self) -> None:
339
+ self.close()
340
+
341
+ def __enter__(self) -> Client:
342
+ self._ensure_open()
343
+ return self
344
+
345
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
346
+ self.close()
ase_sdk/exceptions.py ADDED
@@ -0,0 +1,26 @@
1
+ """Errors deliberately omit signed URLs, credentials, and response bodies."""
2
+
3
+
4
+ class ASEError(Exception):
5
+ """Base class for SDK transport and response errors."""
6
+
7
+
8
+ class HTTPError(ASEError):
9
+ """An unsuccessful HTTP response. Inspect ``body`` explicitly if needed."""
10
+
11
+ def __init__(self, status_code: int, body: bytes) -> None:
12
+ self.status_code = status_code
13
+ self.body = body
14
+ super().__init__(f"ASE HTTP request failed (status {status_code})")
15
+
16
+
17
+ class TransportError(ASEError):
18
+ """HTTP or WebSocket connection, send, receive, or timeout failure."""
19
+
20
+
21
+ class ProtocolError(ASEError):
22
+ """A response isn't a JSON object. Use a raw method to obtain its bytes."""
23
+
24
+
25
+ class ClientClosedError(ASEError):
26
+ """The client was closed; create a new client for another session."""
ase_sdk/models.py ADDED
@@ -0,0 +1,104 @@
1
+ """Request envelopes and media fields compatible with ase-sdk-go/types.go."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass, field, fields, is_dataclass
7
+ from enum import IntEnum
8
+ from typing import Any
9
+
10
+
11
+ class DataStatus(IntEnum):
12
+ FIRST_FRAME = 0
13
+ CONTINUE = 1
14
+ LAST_FRAME = 2
15
+ ONCE = 3
16
+
17
+
18
+ StatusFirstFrame = DataStatus.FIRST_FRAME
19
+ StatusContinue = DataStatus.CONTINUE
20
+ StatusLastFrame = DataStatus.LAST_FRAME
21
+ StatusForOnce = DataStatus.ONCE
22
+
23
+
24
+ class RequestHeader(dict[str, Any]):
25
+ def set_app_id(self, app_id: str) -> RequestHeader:
26
+ self["app_id"] = app_id
27
+ return self
28
+
29
+ def set_status(self, status: int) -> RequestHeader:
30
+ self["status"] = status
31
+ return self
32
+
33
+ def set_res_id(self, res_id: str) -> RequestHeader:
34
+ self["res_id"] = res_id
35
+ return self
36
+
37
+ def set_direct_eng(self, address: str) -> RequestHeader:
38
+ self["directEngIp"] = address
39
+ return self
40
+
41
+
42
+ def to_jsonable(value: Any) -> Any:
43
+ """Copy supported containers without mutating caller-owned request data."""
44
+ if is_dataclass(value) and not isinstance(value, type):
45
+ return {item.name: to_jsonable(getattr(value, item.name)) for item in fields(value)}
46
+ if isinstance(value, Mapping):
47
+ if not all(isinstance(key, str) for key in value):
48
+ raise TypeError("JSON object keys must be strings")
49
+ return {key: to_jsonable(item) for key, item in value.items()}
50
+ if isinstance(value, (list, tuple)):
51
+ return [to_jsonable(item) for item in value]
52
+ return value
53
+
54
+
55
+ @dataclass
56
+ class Request:
57
+ """AIPAAS request. Empty sections are omitted, matching Go's omitempty."""
58
+
59
+ header: Mapping[str, Any] = field(default_factory=RequestHeader)
60
+ parameter: Mapping[str, Any] = field(default_factory=dict)
61
+ payload: Mapping[str, Any] = field(default_factory=dict)
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ return {key: value for key, value in to_jsonable(self).items() if value}
65
+
66
+
67
+ @dataclass
68
+ class AIaaSRequest:
69
+ """AIAAS request with the legacy common/business/data envelope."""
70
+
71
+ common: Mapping[str, Any] = field(default_factory=dict)
72
+ business: Mapping[str, Any] = field(default_factory=dict)
73
+ data: Mapping[str, Any] = field(default_factory=dict)
74
+
75
+ def to_dict(self) -> dict[str, Any]:
76
+ return {key: value for key, value in to_jsonable(self).items() if value}
77
+
78
+
79
+ AIPAASRequest = Request
80
+ AIAASRequest = AIaaSRequest
81
+
82
+
83
+ @dataclass
84
+ class TextPayload:
85
+ text: str
86
+ status: int = DataStatus.ONCE
87
+
88
+
89
+ @dataclass
90
+ class AudioPayload:
91
+ audio: str
92
+ encoding: str
93
+ sample_rate: int
94
+ channels: int = 1
95
+ bit_depth: int = 16
96
+ status: int = DataStatus.ONCE
97
+ seq: int = 0
98
+ frame_size: int = 0
99
+
100
+
101
+ @dataclass
102
+ class ImagePayload:
103
+ image: str
104
+ status: int = DataStatus.ONCE
ase_sdk/py.typed ADDED
File without changes
@@ -0,0 +1,301 @@
1
+ Metadata-Version: 2.4
2
+ Name: ase-sdk-python
3
+ Version: 0.1.0
4
+ Summary: Python SDK for ASE AIpaas and AIaaS HTTP and WebSocket services
5
+ License-Expression: Apache-2.0
6
+ Keywords: ase,aipaas,aiaas,sdk,websocket
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests<3,>=2.32.3
22
+ Requires-Dist: websockets<18,>=15.0.1
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.2.2; extra == "dev"
25
+ Requires-Dist: twine>=6.1; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # ase-sdk-python
29
+
30
+ ASE 服务的同步 Python SDK,支持 **AIpaas** 和 **AIaaS** 两种请求格式,以及 HTTP 和 WebSocket 调用。
31
+
32
+ 实现参考 [iflytek/ase-sdk-go](https://github.com/iflytek/ase-sdk-go/tree/cf7591bcb340a30c78a0036c30f8a572ee84b9e0),固定基线为 `cf7591bcb340a30c78a0036c30f8a572ee84b9e0`。Python 发行包名是 `ase-sdk-python`,导入名是 `ase_sdk`。
33
+
34
+ ## 安装
35
+
36
+ 需要 Python 3.10 或更新版本:
37
+
38
+ ```bash
39
+ python -m pip install ase-sdk-python
40
+ ```
41
+
42
+ 在源码目录中开发:
43
+
44
+ ```bash
45
+ python3 -m venv .venv
46
+ source .venv/bin/activate
47
+ python -m pip install -e '.[dev]'
48
+ python -m unittest discover -s tests -v
49
+ ```
50
+
51
+ ## 凭证与服务地址
52
+
53
+ 以下示例从环境变量读取配置:
54
+
55
+ ```bash
56
+ export ASE_APP_ID='你的 app_id'
57
+ export ASE_API_KEY='你的 api_key'
58
+ export ASE_API_SECRET='你的 api_secret'
59
+ export ASE_HOST='实际服务域名或主机:端口'
60
+ export ASE_URI='/实际服务路径'
61
+ ```
62
+
63
+ `host` 不包含协议或路径,`uri` 以 `/` 开头。默认 `tls=True`,HTTP 使用 HTTPS,WebSocket 使用 WSS;需要访问明确提供明文协议的本地服务时,可显式设置 `tls=False`。
64
+
65
+ 地址必须使用 ASCII:国际化域名先转换为 IDNA,非 ASCII 路径先进行百分号编码;`uri` 不包含查询参数或片段。客户端会先规范化地址,再对实际发送的主机名和路径签名。
66
+
67
+ `app_id` 是请求体中的应用标识;`api_key` 用于标识签名凭证;`api_secret` 仅用于本地计算 HMAC,不作为请求字段发送。SDK 会在缺省时填充 `header.app_id`(AIpaas)或 `common.app_id`(AIaaS)。若请求显式提供了与客户端不一致的 `app_id`,会抛出 `ValueError`。
68
+
69
+ ## HTTP 单次调用
70
+
71
+ ### AIpaas
72
+
73
+ ```python
74
+ import base64
75
+ import os
76
+
77
+ from ase_sdk import Client
78
+
79
+ request = {
80
+ "header": {},
81
+ "parameter": {},
82
+ "payload": {
83
+ "input": { # 按服务协议替换参数名和内容
84
+ "text": base64.b64encode("你好".encode("utf-8")).decode("ascii"),
85
+ "status": 3,
86
+ }
87
+ },
88
+ }
89
+
90
+ with Client(
91
+ app_id=os.environ["ASE_APP_ID"],
92
+ api_key=os.environ["ASE_API_KEY"],
93
+ api_secret=os.environ["ASE_API_SECRET"],
94
+ host=os.environ["ASE_HOST"],
95
+ uri=os.environ["ASE_URI"],
96
+ mode="aipaas", # 默认值
97
+ ) as client:
98
+ response = client.once(request)
99
+ print(response)
100
+ ```
101
+
102
+ AIpaas 单次 HTTP 调用默认补充缺失的 `header.status=3`。显式传入的状态会保留。请求的其他业务参数遵循目标服务的协议。
103
+
104
+ ### AIaaS
105
+
106
+ ```python
107
+ import os
108
+
109
+ from ase_sdk import Client
110
+
111
+ request = {
112
+ "common": {},
113
+ "business": {}, # 按服务协议填写业务参数
114
+ "data": {}, # 按服务协议填写数据和状态
115
+ }
116
+
117
+ with Client(
118
+ app_id=os.environ["ASE_APP_ID"],
119
+ api_key=os.environ["ASE_API_KEY"],
120
+ api_secret=os.environ["ASE_API_SECRET"],
121
+ host=os.environ["ASE_HOST"],
122
+ uri=os.environ["ASE_URI"],
123
+ mode="aiaas",
124
+ ) as client:
125
+ response = client.once(request)
126
+ print(response)
127
+ ```
128
+
129
+ `mode="aiaas"` 时,`once()` 使用 AIaaS 请求格式和 HTTP 鉴权。也可以调用 `once_aiaas(request)`,为本次 HTTP 调用显式选用 AIaaS 格式和鉴权。AIaaS 不自动补充状态字段。
130
+
131
+ SDK **不会自动对媒体内容进行 Base64 编码或解码**,也不推断音频格式、采样率、帧大小或业务字段。示例中的编码由调用方完成;如果目标服务接收明文,应直接传明文。
132
+
133
+ ## WebSocket 调用
134
+
135
+ ```python
136
+ import os
137
+
138
+ from ase_sdk import Client
139
+
140
+ with Client(
141
+ app_id=os.environ["ASE_APP_ID"],
142
+ api_key=os.environ["ASE_API_KEY"],
143
+ api_secret=os.environ["ASE_API_SECRET"],
144
+ host=os.environ["ASE_HOST"],
145
+ uri=os.environ["ASE_URI"],
146
+ mode="aipaas",
147
+ ) as client:
148
+ client.connect()
149
+ client.send({
150
+ "header": {"status": 2},
151
+ "parameter": {},
152
+ "payload": {}, # 按服务协议填入单帧请求数据
153
+ })
154
+ while True:
155
+ response = client.receive()
156
+ print(response)
157
+ header = response.get("header", {})
158
+ if header.get("code", 0) != 0 or header.get("status") == 2:
159
+ break
160
+ ```
161
+
162
+ 上述结束条件是常见的 AIpaas 响应约定;请使用实际服务定义的状态和错误字段。AIaaS 通过 `mode="aiaas"` 配合 `send()`,或直接使用 `send_aiaas()`,发送 `common / business / data` 格式。AIaaS 响应通常需要按服务协议检查 `code`、`data.status` 等字段。
163
+
164
+ 两种模式的 WebSocket 握手都使用与 Go SDK 一致的 **GET 签名 URL**;AIaaS 的 HTTP Digest 鉴权不用于 WebSocket。
165
+
166
+ `with` 负责退出时清理资源,不主动建立 WebSocket。`connect()` 可显式建立连接;`send()` 和 `receive()` 也会在需要时首次连接。WebSocket 状态由调用方提供,SDK 不自动添加首帧或末帧标志。
167
+
168
+ 同一个客户端支持一个发送线程与一个接收线程并发,适合边上传音频边读取结果。不要在同一连接上创建多个接收线程;多个独立会话应使用各自的 `Client`。SDK 不自动重连或重放 WebSocket 帧。
169
+
170
+ 源码包的 `examples/` 目录提供 `http_aipaas.py`、`http_aiaas.py` 和 `websocket_stream.py`,支持从 JSON 文件读取目标服务的真实请求;流式示例使用 JSONL 文件逐帧发送,同时接收结果。
171
+
172
+ ## API
173
+
174
+ ```python
175
+ Client(
176
+ app_id, api_key, api_secret, host, uri,
177
+ *, mode="aipaas", tls=True,
178
+ timeout=30, retries=0, retry_backoff=0.5,
179
+ handshake_timeout=10, read_timeout=30, write_timeout=30, close_timeout=5,
180
+ max_size=16 * 1024 * 1024, connect_headers=None,
181
+ )
182
+ ```
183
+
184
+ | 参数 | 用途 |
185
+ | --- | --- |
186
+ | `mode` | `aipaas` 或 `aiaas`,决定默认请求格式和 HTTP 鉴权 |
187
+ | `tls` | 是否使用 HTTPS / WSS,默认开启并校验证书 |
188
+ | `timeout` | HTTP 请求超时,单位秒 |
189
+ | `retries` | HTTP 请求失败后的最大重试次数,默认 `0` |
190
+ | `retry_backoff` | HTTP 重试的初始退避秒数 |
191
+ | `handshake_timeout` | WebSocket 连接握手超时,单位秒 |
192
+ | `read_timeout` | 等待单条 WebSocket 消息的超时,单位秒 |
193
+ | `write_timeout` | 发送单条 WebSocket 消息的超时,单位秒;超时后中断连接 |
194
+ | `close_timeout` | WebSocket 关闭超时,单位秒;超时后中断底层连接 |
195
+ | `max_size` | WebSocket 接收单条消息的最大字节数,默认 16 MiB |
196
+ | `connect_headers` | WebSocket 握手时附加的请求头,不能覆盖鉴权字段或协议握手字段 |
197
+
198
+ 超时参数可设为 `None` 以禁用对应超时,`max_size=None` 可禁用接收大小限制。HTTP 会保留环境代理配置,但不会读取 `.netrc` 中的 Basic 鉴权覆盖 ASE 签名;WebSocket 与 Go SDK 一样直接连接服务。
199
+
200
+ | 方法 | 返回值 / 行为 |
201
+ | --- | --- |
202
+ | `once(request)` | 按客户端模式发送 HTTP POST,返回解析后的 JSON 字典 |
203
+ | `once_raw(request)` | 同上,返回原始响应 `bytes` |
204
+ | `once_aiaas(request)` | 显式使用 AIaaS HTTP POST,返回 JSON 字典 |
205
+ | `once_aiaas_raw(request)` | 显式使用 AIaaS HTTP POST,返回 `bytes` |
206
+ | `connect()` | 建立 WebSocket 连接 |
207
+ | `send(request)` | 按客户端模式发送一条 WebSocket JSON 消息 |
208
+ | `send_aiaas(request)` | 显式发送一条 AIaaS WebSocket JSON 消息 |
209
+ | `receive()` | 读取一条 WebSocket 消息并解析为 JSON 字典 |
210
+ | `receive_raw()` | 读取一条 WebSocket 消息,返回 `bytes` |
211
+ | `close()` / `destroy()` | 关闭连接并释放客户端资源 |
212
+
213
+ 字典是最直接的请求表达方式,也可使用导出的 `Request` / `AIPAASRequest`、`AIaaSRequest` / `AIAASRequest`、`RequestHeader`、`TextPayload`、`AudioPayload`、`ImagePayload` 和 `DataStatus` 模型。状态值与 Go SDK 一致:首帧 `0`、中间帧 `1`、末帧 `2`、单次请求 `3`。
214
+
215
+ ### 错误与重试
216
+
217
+ - `HTTPError`:HTTP 非成功响应;`status_code` 和 `body` 提供状态码及原始响应正文。
218
+ - `TransportError`:网络、连接或超时等传输错误。
219
+ - `ProtocolError`:响应不是预期的 JSON 对象等协议错误。
220
+ - `ClientClosedError`:客户端已经关闭。
221
+ - 上述 SDK 异常均继承 `ASEError`;无效参数可抛出 `ValueError` 或 `TypeError`。
222
+
223
+ 服务业务错误码不为零时,SDK 仍然返回响应,由调用方按实际响应协议判断。原始响应可能包含业务数据,异常的字符串表示不会自动输出签名 URL 或响应正文。
224
+
225
+ 默认不重试 HTTP POST。显式增加 `retries` 可能造成服务重复执行或重复计费,只有在调用方能够接受或处理重复执行时才开启。WebSocket 不自动重试。
226
+
227
+ ## 鉴权与 Go SDK 的对应关系
228
+
229
+ ### AIpaas HTTP,以及两种模式的 WebSocket
230
+
231
+ 待签名字符串使用换行符 `\n`,末尾不额外增加换行:
232
+
233
+ ```text
234
+ host: {host}
235
+ date: {date}
236
+ {method} {uri} HTTP/1.1
237
+ ```
238
+
239
+ HTTP 的 `method` 为 `POST`,WebSocket 为 `GET`。先计算 `Base64(HMAC-SHA256(api_secret, 签名原串))`,再组装:
240
+
241
+ ```text
242
+ api_key="{api_key}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature}"
243
+ ```
244
+
245
+ 将整个鉴权描述再次 Base64 编码作为 `authorization`,连同 `date` 和 `host` 一起作为 URL 查询参数进行 URL 编码。
246
+
247
+ ### AIaaS HTTP
248
+
249
+ 先对实际发送的 JSON 请求体字节计算:
250
+
251
+ ```text
252
+ Digest: SHA-256={Base64(SHA256(body_bytes))}
253
+ ```
254
+
255
+ 待签名字符串为:
256
+
257
+ ```text
258
+ host: {host}
259
+ date: {date}
260
+ POST {uri} HTTP/1.1
261
+ digest: SHA-256={body_sha256_base64}
262
+ ```
263
+
264
+ 计算 `Base64(HMAC-SHA256(api_secret, 签名原串))` 后,在请求头中发送:
265
+
266
+ ```text
267
+ Authorization: hmac api_key="{api_key}", algorithm="hmac-sha256", headers="host date request-line digest", signature="{signature}"
268
+ Host: {host}
269
+ Date: {date}
270
+ Digest: SHA-256={body_sha256_base64}
271
+ Content-Type: application/json
272
+ ```
273
+
274
+ 该 Authorization 值包含 `hmac` 前缀,不做第二层 Base64 编码。SDK 对请求体只序列化一次,签名和发送使用相同字节,避免 JSON 空白、字段顺序或字符编码差异引发验签失败。日期采用 UTC 时间;服务端通常要求客户端时钟偏差不超过 300 秒。
275
+
276
+ ## 构建和发布
277
+
278
+ 发布流程参考同工作区 AIGES 的 `pyaiges`,产出源码包和 wheel,并运行严格元数据检查:
279
+
280
+ ```bash
281
+ python -m unittest discover -s tests -v
282
+ PYTHON=.venv/bin/python ./publish_pypi.sh --build-only
283
+ ```
284
+
285
+ 脚本要求指定解释器中已安装 `build` 和 `twine`,不会自动修改全局 Python 环境。验证通过的产物会复制到 `dist/`。上传仅使用本次构建的两个产物,不会上传 `dist/` 中的历史版本。
286
+
287
+ 凭证可配置在 `~/.pypirc`,也可由 `PYPI_TOKEN` 或 `TWINE_USERNAME=__token__` / `TWINE_PASSWORD` 环境变量提供。令牌通过环境变量传给 Twine,不放入命令行参数。配置好凭证后执行:
288
+
289
+ ```bash
290
+ # 正式 PyPI
291
+ PYTHON=.venv/bin/python ./publish_pypi.sh
292
+
293
+ # TestPyPI 使用独立的 TestPyPI 凭证
294
+ PYTHON=.venv/bin/python ./publish_pypi.sh --test
295
+ ```
296
+
297
+ 测试覆盖固定 Go 鉴权向量以及本地 HTTP / WebSocket 服务的真实传输;接入实际引擎时还需使用目标服务的业务参数和凭证验证。PyPI 同一版本的发行文件不可覆盖;后续发布需要更新 `pyproject.toml`、SDK 版本和 `CHANGELOG.md`。
298
+
299
+ ## 许可证
300
+
301
+ Apache License 2.0,完整许可文本随发行包的 `LICENSE` 文件提供。
@@ -0,0 +1,11 @@
1
+ ase_sdk/__init__.py,sha256=TOVFd1zV33AB7V9MwHGoDyr648uULuthPH51T_jDv2k,884
2
+ ase_sdk/auth.py,sha256=wVe908EchUArqpSSj6h10bdEp0PXb9lb8XRoaQMH308,6759
3
+ ase_sdk/client.py,sha256=h2LDmMLyDc_z3r0sF6eHGIVQj0j7Fi9TjtAWmYE75lE,16266
4
+ ase_sdk/exceptions.py,sha256=XEAE-OIws5BHgfBWVL7kgKJn4BoOaYUz7uYdzOYNLY0,813
5
+ ase_sdk/models.py,sha256=lGUwmmlwSPMSoeol4p8_sDoQmWpGbGxJ65fSumpoNIY,2865
6
+ ase_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ ase_sdk_python-0.1.0.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
8
+ ase_sdk_python-0.1.0.dist-info/METADATA,sha256=LcUvhG4klNMmfizr-fYwdE1eB-KVx4GtArqYbQM3qM0,12390
9
+ ase_sdk_python-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ ase_sdk_python-0.1.0.dist-info/top_level.txt,sha256=FKBjT6atVWte8BYDvKUd7HPtVkXTdKEv7tqJMq6odms,8
11
+ ase_sdk_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright {yyyy} {name of copyright owner}
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1 @@
1
+ ase_sdk