altertable-lakehouse 0.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Altertable
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: altertable-lakehouse
3
+ Version: 0.2.0
4
+ Summary: Official Python SDK for Altertable Lakehouse API
5
+ License-File: LICENSE
6
+ Author: Altertable
7
+ Author-email: hello@altertable.ai
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: dataclasses-json (>=0.6.7,<0.7.0)
17
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
18
+ Requires-Dist: pydantic (>=2.12.5,<3.0.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Altertable Lakehouse Python SDK
22
+
23
+ Official Python SDK for the Altertable Lakehouse API.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install altertable-lakehouse
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### Initialization
34
+
35
+ ```python
36
+ from altertable_lakehouse import Client
37
+
38
+ client = Client(username="your_username", password="your_password")
39
+ ```
40
+
41
+ ### Querying
42
+
43
+ ```python
44
+ from altertable_lakehouse.models import QueryRequest
45
+
46
+ # Stream rows (good for large datasets)
47
+ req = QueryRequest(statement="SELECT * FROM my_table")
48
+ metadata, row_iterator = client.query(req)
49
+ for row in row_iterator:
50
+ print(row)
51
+
52
+ # Accumulate all rows in memory
53
+ result = client.query_all(req)
54
+ print(result.rows)
55
+ ```
56
+
57
+ ### Append
58
+
59
+ ```python
60
+ res = client.append(catalog="my_cat", schema="my_schema", table="my_table", data={"col1": "val1"})
61
+ print(res.ok)
62
+ ```
63
+
64
+ ### Upload
65
+
66
+ ```python
67
+ from altertable_lakehouse.models import UploadFormat, UploadMode
68
+
69
+ with open("data.csv", "rb") as f:
70
+ client.upload(
71
+ catalog="my_cat",
72
+ schema="my_schema",
73
+ table="my_table",
74
+ format=UploadFormat.CSV,
75
+ mode=UploadMode.APPEND,
76
+ content=f.read()
77
+ )
78
+ ```
79
+
80
+ ### Validate Query
81
+
82
+ ```python
83
+ res = client.validate("SELECT * FROM non_existent")
84
+ print(res.valid)
85
+ print(res.connections_errors)
86
+ ```
87
+
88
+ ### Query Log & Cancellation
89
+
90
+ ```python
91
+ # Get query status
92
+ log_res = client.get_query("query_uuid_here")
93
+ print(log_res.progress)
94
+
95
+ # Cancel a query
96
+ cancel_res = client.cancel_query("query_uuid_here", "session_id_here")
97
+ print(cancel_res.cancelled)
98
+ ```
99
+
@@ -0,0 +1,78 @@
1
+ # Altertable Lakehouse Python SDK
2
+
3
+ Official Python SDK for the Altertable Lakehouse API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install altertable-lakehouse
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Initialization
14
+
15
+ ```python
16
+ from altertable_lakehouse import Client
17
+
18
+ client = Client(username="your_username", password="your_password")
19
+ ```
20
+
21
+ ### Querying
22
+
23
+ ```python
24
+ from altertable_lakehouse.models import QueryRequest
25
+
26
+ # Stream rows (good for large datasets)
27
+ req = QueryRequest(statement="SELECT * FROM my_table")
28
+ metadata, row_iterator = client.query(req)
29
+ for row in row_iterator:
30
+ print(row)
31
+
32
+ # Accumulate all rows in memory
33
+ result = client.query_all(req)
34
+ print(result.rows)
35
+ ```
36
+
37
+ ### Append
38
+
39
+ ```python
40
+ res = client.append(catalog="my_cat", schema="my_schema", table="my_table", data={"col1": "val1"})
41
+ print(res.ok)
42
+ ```
43
+
44
+ ### Upload
45
+
46
+ ```python
47
+ from altertable_lakehouse.models import UploadFormat, UploadMode
48
+
49
+ with open("data.csv", "rb") as f:
50
+ client.upload(
51
+ catalog="my_cat",
52
+ schema="my_schema",
53
+ table="my_table",
54
+ format=UploadFormat.CSV,
55
+ mode=UploadMode.APPEND,
56
+ content=f.read()
57
+ )
58
+ ```
59
+
60
+ ### Validate Query
61
+
62
+ ```python
63
+ res = client.validate("SELECT * FROM non_existent")
64
+ print(res.valid)
65
+ print(res.connections_errors)
66
+ ```
67
+
68
+ ### Query Log & Cancellation
69
+
70
+ ```python
71
+ # Get query status
72
+ log_res = client.get_query("query_uuid_here")
73
+ print(log_res.progress)
74
+
75
+ # Cancel a query
76
+ cancel_res = client.cancel_query("query_uuid_here", "session_id_here")
77
+ print(cancel_res.cancelled)
78
+ ```
@@ -0,0 +1,23 @@
1
+ [tool.poetry]
2
+ name = "altertable-lakehouse"
3
+ version = "0.2.0"
4
+ description = "Official Python SDK for Altertable Lakehouse API"
5
+ authors = ["Altertable <hello@altertable.ai>"]
6
+ readme = "README.md"
7
+ packages = [{include = "altertable_lakehouse", from = "src"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.9"
11
+ httpx = "^0.28.1"
12
+ dataclasses-json = "^0.6.7"
13
+ pydantic = "^2.12.5"
14
+
15
+ [tool.poetry.group.dev.dependencies]
16
+ pytest = "^8.0.0"
17
+ ruff = "^0.3.0"
18
+ mypy = "^1.8.0"
19
+ testcontainers = "<4.0"
20
+
21
+ [build-system]
22
+ requires = ["poetry-core"]
23
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,5 @@
1
+ from .client import Client
2
+ from . import models
3
+ from . import errors
4
+
5
+ __all__ = ["Client", "models", "errors"]
@@ -0,0 +1,186 @@
1
+ import os
2
+ import json
3
+ import base64
4
+ import httpx
5
+ from typing import Any, Dict, Iterator, Optional, Union, Tuple, NoReturn
6
+ from .models import (
7
+ AppendRequestSingle, AppendRequestBatch, AppendResponse,
8
+ QueryRequest, QueryLogResponse, CancelQueryResponse,
9
+ ValidateResponse, UploadFormat, UploadMode,
10
+ QueryMetadata, QueryResult
11
+ )
12
+ from .errors import (
13
+ AuthError, BadRequestError, NetworkError, TimeoutError,
14
+ ParseError, ApiError, ConfigurationError,
15
+ AltertableLakehouseError
16
+ )
17
+
18
+ class Client:
19
+ def __init__(
20
+ self,
21
+ base_url: str = "https://api.altertable.ai",
22
+ username: Optional[str] = None,
23
+ password: Optional[str] = None,
24
+ token: Optional[str] = None,
25
+ timeout: float = 30.0,
26
+ user_agent_suffix: Optional[str] = None
27
+ ):
28
+ self.base_url = base_url.rstrip("/")
29
+ self.timeout = timeout
30
+
31
+ auth_token = None
32
+ if token:
33
+ auth_token = token
34
+ elif username and password:
35
+ auth_token = base64.b64encode(f"{username}:{password}".encode()).decode()
36
+ elif "ALTERTABLE_BASIC_AUTH_TOKEN" in os.environ:
37
+ auth_token = os.environ["ALTERTABLE_BASIC_AUTH_TOKEN"]
38
+ elif "ALTERTABLE_USERNAME" in os.environ and "ALTERTABLE_PASSWORD" in os.environ:
39
+ u = os.environ["ALTERTABLE_USERNAME"]
40
+ p = os.environ["ALTERTABLE_PASSWORD"]
41
+ auth_token = base64.b64encode(f"{u}:{p}".encode()).decode()
42
+
43
+ if not auth_token:
44
+ raise ConfigurationError("No credentials provided.")
45
+
46
+ ua = "altertable-lakehouse-python/0.1.0"
47
+ if user_agent_suffix:
48
+ ua += f" {user_agent_suffix}"
49
+
50
+ self._client = httpx.Client(
51
+ base_url=self.base_url,
52
+ timeout=self.timeout,
53
+ headers={
54
+ "Authorization": f"Basic {auth_token}",
55
+ "User-Agent": ua
56
+ }
57
+ )
58
+
59
+ def _handle_error(self, e: Exception) -> NoReturn:
60
+ if isinstance(e, httpx.TimeoutException):
61
+ raise TimeoutError("Request timed out", e)
62
+ if isinstance(e, httpx.RequestError):
63
+ raise NetworkError(f"Network error: {str(e)}", e)
64
+ raise AltertableLakehouseError(f"Unexpected error: {str(e)}", e)
65
+
66
+ def _check_response(self, response: httpx.Response) -> None:
67
+ if response.is_success:
68
+ return
69
+ if response.status_code == 401:
70
+ raise AuthError("Unauthorized", response.status_code)
71
+ if response.status_code == 400:
72
+ raise BadRequestError(response.text, response.status_code)
73
+ raise ApiError(response.text, response.status_code)
74
+
75
+ def append(self, catalog: str, schema: str, table: str, data: Union[AppendRequestSingle, AppendRequestBatch]) -> AppendResponse:
76
+ try:
77
+ payload = data.model_dump() if hasattr(data, "model_dump") else data
78
+ res = self._client.post(
79
+ "/append",
80
+ params={"catalog": catalog, "schema": schema, "table": table},
81
+ json=payload
82
+ )
83
+ self._check_response(res)
84
+ return AppendResponse(**res.json())
85
+ except httpx.RequestError as e:
86
+ self._handle_error(e)
87
+
88
+ def upload(self, catalog: str, schema: str, table: str, format: UploadFormat, mode: UploadMode, content: bytes, primary_key: Optional[str] = None) -> None:
89
+ params = {
90
+ "catalog": catalog,
91
+ "schema": schema,
92
+ "table": table,
93
+ "format": format.value,
94
+ "mode": mode.value
95
+ }
96
+ if primary_key:
97
+ params["primary_key"] = primary_key
98
+ try:
99
+ res = self._client.post(
100
+ "/upload",
101
+ params=params,
102
+ content=content,
103
+ headers={"Content-Type": "application/octet-stream"}
104
+ )
105
+ self._check_response(res)
106
+ except httpx.RequestError as e:
107
+ self._handle_error(e)
108
+
109
+ def get_query(self, query_id: str) -> QueryLogResponse:
110
+ try:
111
+ res = self._client.get(f"/query/{query_id}")
112
+ self._check_response(res)
113
+ return QueryLogResponse(**res.json())
114
+ except httpx.RequestError as e:
115
+ self._handle_error(e)
116
+
117
+ def cancel_query(self, query_id: str, session_id: str) -> CancelQueryResponse:
118
+ try:
119
+ res = self._client.delete(f"/query/{query_id}", params={"session_id": session_id})
120
+ self._check_response(res)
121
+ return CancelQueryResponse(**res.json())
122
+ except httpx.RequestError as e:
123
+ self._handle_error(e)
124
+
125
+ def validate(self, statement: str) -> ValidateResponse:
126
+ try:
127
+ res = self._client.post("/validate", json={"statement": statement})
128
+ self._check_response(res)
129
+ return ValidateResponse(**res.json())
130
+ except httpx.RequestError as e:
131
+ self._handle_error(e)
132
+
133
+ def query(self, request: QueryRequest) -> Tuple[QueryMetadata, Iterator[Dict[str, Any]]]:
134
+ payload = request.model_dump(exclude_none=True)
135
+ try:
136
+ req = self._client.build_request(
137
+ "POST",
138
+ "/query",
139
+ json=payload,
140
+ headers={"Accept": "application/x-ndjson"}
141
+ )
142
+ res = self._client.send(req, stream=True)
143
+ self._check_response(res)
144
+
145
+ def parse_stream() -> Iterator[Dict[str, Any]]:
146
+ line_index = 0
147
+ for line in res.iter_lines():
148
+ if not line.strip():
149
+ continue
150
+ try:
151
+ yield json.loads(line)
152
+ except json.JSONDecodeError as exc:
153
+ raise ParseError("Failed to parse NDJSON line", line_index, line) from exc
154
+ line_index += 1
155
+
156
+ iterator = parse_stream()
157
+ metadata = QueryMetadata()
158
+
159
+ first = next(iterator, None)
160
+ if first and "metadata" in first:
161
+ metadata.stats = first.get("metadata", {})
162
+ second = next(iterator, None)
163
+ if second and "columns" in second:
164
+ metadata.columns = second.get("columns", [])
165
+ else:
166
+ def row_generator1(first_row: Any, iter_obj: Iterator[Any]) -> Iterator[Any]:
167
+ if first_row:
168
+ yield first_row
169
+ yield from iter_obj
170
+ return metadata, row_generator1(second, iterator)
171
+ else:
172
+ def row_generator2(first_row: Any, iter_obj: Iterator[Any]) -> Iterator[Any]:
173
+ if first_row:
174
+ yield first_row
175
+ yield from iter_obj
176
+ return metadata, row_generator2(first, iterator)
177
+
178
+ return metadata, iterator
179
+
180
+ except httpx.RequestError as e:
181
+ self._handle_error(e)
182
+
183
+ def query_all(self, request: QueryRequest) -> QueryResult:
184
+ metadata, iterator = self.query(request)
185
+ rows = list(iterator)
186
+ return QueryResult(metadata=metadata, rows=rows)
@@ -0,0 +1,35 @@
1
+ from typing import Optional
2
+
3
+ class AltertableLakehouseError(Exception):
4
+ def __init__(self, message: str, cause: Optional[Exception] = None):
5
+ super().__init__(message)
6
+ self.cause = cause
7
+
8
+ class NetworkError(AltertableLakehouseError):
9
+ pass
10
+
11
+ class TimeoutError(AltertableLakehouseError):
12
+ pass
13
+
14
+ class SerializationError(AltertableLakehouseError):
15
+ pass
16
+
17
+ class ParseError(AltertableLakehouseError):
18
+ def __init__(self, message: str, line_index: int, raw_content: str):
19
+ super().__init__(f"{message} at line {line_index}: {raw_content}")
20
+ self.line_index = line_index
21
+ self.raw_content = raw_content
22
+
23
+ class ApiError(AltertableLakehouseError):
24
+ def __init__(self, message: str, status_code: int):
25
+ super().__init__(f"{status_code}: {message}")
26
+ self.status_code = status_code
27
+
28
+ class AuthError(ApiError):
29
+ pass
30
+
31
+ class BadRequestError(ApiError):
32
+ pass
33
+
34
+ class ConfigurationError(AltertableLakehouseError):
35
+ pass
@@ -0,0 +1,78 @@
1
+ from typing import Any, Dict, List, Optional, Literal, Union
2
+ from enum import Enum
3
+ from pydantic import BaseModel, Field
4
+
5
+ class ComputeSize(str, Enum):
6
+ S = "S"
7
+ M = "M"
8
+ L = "L"
9
+
10
+ class UploadFormat(str, Enum):
11
+ CSV = "csv"
12
+ JSON = "json"
13
+ PARQUET = "parquet"
14
+
15
+ class UploadMode(str, Enum):
16
+ CREATE = "create"
17
+ APPEND = "append"
18
+ UPSERT = "upsert"
19
+ OVERWRITE = "overwrite"
20
+
21
+ AppendRequestSingle = Dict[str, Any]
22
+ AppendRequestBatch = List[Dict[str, Any]]
23
+ AppendRequest = Union[AppendRequestSingle, AppendRequestBatch]
24
+
25
+ class AppendResponse(BaseModel):
26
+ ok: bool
27
+ error_code: Optional[Literal["invalid-data"]] = None
28
+
29
+ class QueryRequest(BaseModel):
30
+ statement: str
31
+ catalog: Optional[str] = None
32
+ schema_: Optional[str] = Field(default=None, alias="schema")
33
+ session_id: Optional[str] = None
34
+ compute_size: Optional[ComputeSize] = None
35
+ sanitize: Optional[bool] = None
36
+ limit: Optional[int] = None
37
+ offset: Optional[int] = None
38
+ timezone: Optional[str] = None
39
+ ephemeral: Optional[bool] = None
40
+ visible: Optional[bool] = None
41
+ requested_by: Optional[str] = None
42
+ query_id: Optional[str] = None
43
+
44
+ class QueryLogResponse(BaseModel):
45
+ uuid: str
46
+ start_time: str
47
+ end_time: str
48
+ duration_ms: int
49
+ query: str
50
+ session_id: Optional[str] = None
51
+ client_interface: str
52
+ error: Optional[str] = None
53
+ stats: Dict[str, Any]
54
+ progress: int
55
+ visible: bool
56
+ requested_by: str
57
+ user_agent: str
58
+
59
+ class CancelQueryResponse(BaseModel):
60
+ cancelled: bool
61
+ message: str
62
+
63
+ class ValidateRequest(BaseModel):
64
+ statement: str
65
+
66
+ class ValidateResponse(BaseModel):
67
+ valid: bool
68
+ statement: str
69
+ connections_errors: Any
70
+ error: Optional[str] = None
71
+
72
+ class QueryMetadata(BaseModel):
73
+ columns: List[Dict[str, Any]] = Field(default_factory=list)
74
+ stats: Dict[str, Any] = Field(default_factory=dict)
75
+
76
+ class QueryResult(BaseModel):
77
+ metadata: QueryMetadata
78
+ rows: List[Any]