renart 0.3.2__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.
- renart/__init__.py +28 -0
- renart/__init__.pyi +7 -0
- renart/_client.py +129 -0
- renart/_client.pyi +15 -0
- renart/context.py +107 -0
- renart/context.pyi +28 -0
- renart/py.typed +1 -0
- renart-0.3.2.dist-info/METADATA +22 -0
- renart-0.3.2.dist-info/RECORD +11 -0
- renart-0.3.2.dist-info/WHEEL +4 -0
- renart-0.3.2.dist-info/top_level.txt +1 -0
renart/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Renart Python SDK.
|
|
2
|
+
|
|
3
|
+
Available inside renart Python assets. `query()` runs read-only SQL against
|
|
4
|
+
the project's connections through the renart runner — credentials never enter
|
|
5
|
+
the Python process — and `context` exposes the run window and metadata.
|
|
6
|
+
|
|
7
|
+
from renart import query, context
|
|
8
|
+
|
|
9
|
+
def materialize():
|
|
10
|
+
games = query("select * from chess_games").to_pandas()
|
|
11
|
+
return games.groupby("winner").size().reset_index(name="wins")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ._client import (
|
|
15
|
+
BrokerUnavailableError,
|
|
16
|
+
QueryError,
|
|
17
|
+
RenartError,
|
|
18
|
+
query,
|
|
19
|
+
)
|
|
20
|
+
from .context import context
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"query",
|
|
24
|
+
"context",
|
|
25
|
+
"RenartError",
|
|
26
|
+
"QueryError",
|
|
27
|
+
"BrokerUnavailableError",
|
|
28
|
+
]
|
renart/__init__.pyi
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from ._client import BrokerUnavailableError as BrokerUnavailableError
|
|
2
|
+
from ._client import QueryError as QueryError
|
|
3
|
+
from ._client import RenartError as RenartError
|
|
4
|
+
from ._client import query as query
|
|
5
|
+
from .context import _Context as _Context
|
|
6
|
+
|
|
7
|
+
context: _Context
|
renart/_client.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""HTTP client for the renart run broker.
|
|
2
|
+
|
|
3
|
+
The runner starts a loopback broker per task and injects RENART_API_URL and
|
|
4
|
+
RENART_API_TOKEN. Queries execute inside the runner on the project's
|
|
5
|
+
connections; results stream back as Arrow IPC. Only the token crosses the
|
|
6
|
+
process boundary — never credentials.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RenartError(Exception):
|
|
19
|
+
"""Base class for renart SDK errors."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BrokerUnavailableError(RenartError):
|
|
23
|
+
"""The run broker is not reachable (not running inside a renart run?)."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class QueryError(RenartError):
|
|
27
|
+
"""The broker rejected or failed to execute a query."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, code: Optional[str] = None):
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.code = code
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _broker_address() -> "tuple[str, str]":
|
|
35
|
+
url = os.environ.get("RENART_API_URL", "").strip()
|
|
36
|
+
token = os.environ.get("RENART_API_TOKEN", "").strip()
|
|
37
|
+
if not url or not token:
|
|
38
|
+
raise BrokerUnavailableError(
|
|
39
|
+
"renart.query() needs the run broker, which is only available while "
|
|
40
|
+
"the asset runs through renart (RENART_API_URL / RENART_API_TOKEN "
|
|
41
|
+
"are not set)."
|
|
42
|
+
)
|
|
43
|
+
return url, token
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _raise_from_response(exc: urllib.error.HTTPError) -> "None":
|
|
47
|
+
code = None
|
|
48
|
+
message = f"broker request failed with HTTP {exc.code}"
|
|
49
|
+
try:
|
|
50
|
+
payload = json.loads(exc.read().decode("utf-8"))
|
|
51
|
+
error = payload.get("error") or {}
|
|
52
|
+
message = error.get("message") or message
|
|
53
|
+
code = error.get("code")
|
|
54
|
+
except Exception: # noqa: BLE001 - keep the HTTP error as the message
|
|
55
|
+
pass
|
|
56
|
+
raise QueryError(message, code) from None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def query(sql: str, connection: Optional[str] = None, format: str = "arrow") -> Any:
|
|
60
|
+
"""Run a read-only SQL query through the renart runner.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
sql: a single SELECT statement. It may reference other assets of the
|
|
64
|
+
project; if a referenced asset is being materialized right now,
|
|
65
|
+
the runner waits for it to finish before executing the query.
|
|
66
|
+
connection: a project connection name; defaults to the asset's own
|
|
67
|
+
connection.
|
|
68
|
+
format: ``"arrow"`` (default) returns a pyarrow.Table,
|
|
69
|
+
``"pandas"`` returns a pandas.DataFrame.
|
|
70
|
+
|
|
71
|
+
Credentials never enter the Python process; the query executes inside
|
|
72
|
+
the renart runner.
|
|
73
|
+
"""
|
|
74
|
+
if format not in ("pandas", "arrow"):
|
|
75
|
+
raise ValueError(f'format must be "pandas" or "arrow", got {format!r}')
|
|
76
|
+
|
|
77
|
+
url, token = _broker_address()
|
|
78
|
+
body = {"sql": sql}
|
|
79
|
+
if connection:
|
|
80
|
+
body["connection"] = connection
|
|
81
|
+
request = urllib.request.Request(
|
|
82
|
+
url + "/v1/query",
|
|
83
|
+
data=json.dumps(body).encode("utf-8"),
|
|
84
|
+
headers={
|
|
85
|
+
"Authorization": "Bearer " + token,
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
},
|
|
88
|
+
method="POST",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
import pyarrow.ipc as ipc
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
response = urllib.request.urlopen(request)
|
|
95
|
+
except urllib.error.HTTPError as exc:
|
|
96
|
+
_raise_from_response(exc)
|
|
97
|
+
except urllib.error.URLError as exc:
|
|
98
|
+
raise BrokerUnavailableError(f"cannot reach the run broker: {exc.reason}") from None
|
|
99
|
+
|
|
100
|
+
with response:
|
|
101
|
+
table = ipc.open_stream(response).read_all()
|
|
102
|
+
|
|
103
|
+
if format == "arrow":
|
|
104
|
+
return table
|
|
105
|
+
try:
|
|
106
|
+
return table.to_pandas()
|
|
107
|
+
except ImportError as exc:
|
|
108
|
+
raise RenartError(
|
|
109
|
+
'query(format="pandas") needs pandas installed in the asset '
|
|
110
|
+
'environment; add pandas to the asset dependencies or use '
|
|
111
|
+
'format="arrow".'
|
|
112
|
+
) from exc
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fetch_context() -> "dict[str, Any]":
|
|
116
|
+
"""Fetch the run context document from the broker (internal)."""
|
|
117
|
+
url, token = _broker_address()
|
|
118
|
+
request = urllib.request.Request(
|
|
119
|
+
url + "/v1/context",
|
|
120
|
+
headers={"Authorization": "Bearer " + token},
|
|
121
|
+
)
|
|
122
|
+
try:
|
|
123
|
+
with urllib.request.urlopen(request) as response:
|
|
124
|
+
return json.loads(response.read().decode("utf-8"))
|
|
125
|
+
except urllib.error.HTTPError as exc:
|
|
126
|
+
_raise_from_response(exc)
|
|
127
|
+
raise # unreachable; _raise_from_response always raises
|
|
128
|
+
except urllib.error.URLError as exc:
|
|
129
|
+
raise BrokerUnavailableError(f"cannot reach the run broker: {exc.reason}") from None
|
renart/_client.pyi
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
class RenartError(Exception): ...
|
|
4
|
+
|
|
5
|
+
class BrokerUnavailableError(RenartError): ...
|
|
6
|
+
|
|
7
|
+
class QueryError(RenartError):
|
|
8
|
+
code: str | None
|
|
9
|
+
def __init__(self, message: str, code: str | None = None) -> None: ...
|
|
10
|
+
|
|
11
|
+
def query(
|
|
12
|
+
sql: str,
|
|
13
|
+
connection: str | None = None,
|
|
14
|
+
format: Literal["arrow", "pandas"] = "arrow",
|
|
15
|
+
) -> Any: ...
|
renart/context.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Typed access to the run context.
|
|
2
|
+
|
|
3
|
+
Backed by the BRUIN_* environment variables the runner injects, so it works
|
|
4
|
+
in any execution mode (and stays compatible with scripts that read the env
|
|
5
|
+
vars directly). Every property returns None when the value is absent, except
|
|
6
|
+
``is_full_refresh`` (False) and ``vars`` (``{}``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from datetime import date, datetime
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _env(name: str) -> Optional[str]:
|
|
18
|
+
value = os.environ.get(name, "").strip()
|
|
19
|
+
return value or None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_date(value: Optional[str]) -> Optional[date]:
|
|
23
|
+
if not value:
|
|
24
|
+
return None
|
|
25
|
+
try:
|
|
26
|
+
return date.fromisoformat(value)
|
|
27
|
+
except ValueError:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_datetime(value: Optional[str]) -> Optional[datetime]:
|
|
32
|
+
if not value:
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
return datetime.fromisoformat(value)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _Context:
|
|
41
|
+
"""Runtime information about the current renart run."""
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def start_date(self) -> Optional[date]:
|
|
45
|
+
"""Start of the run window."""
|
|
46
|
+
return _parse_date(_env("BRUIN_START_DATE"))
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def start_datetime(self) -> Optional[datetime]:
|
|
50
|
+
return _parse_datetime(_env("BRUIN_START_DATETIME"))
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def end_date(self) -> Optional[date]:
|
|
54
|
+
"""End of the run window."""
|
|
55
|
+
return _parse_date(_env("BRUIN_END_DATE"))
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def end_datetime(self) -> Optional[datetime]:
|
|
59
|
+
return _parse_datetime(_env("BRUIN_END_DATETIME"))
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def execution_date(self) -> Optional[datetime]:
|
|
63
|
+
return _parse_datetime(_env("BRUIN_EXECUTION_DATETIME"))
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def pipeline(self) -> Optional[str]:
|
|
67
|
+
return _env("BRUIN_PIPELINE")
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def asset_name(self) -> Optional[str]:
|
|
71
|
+
return _env("BRUIN_ASSET")
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def connection(self) -> Optional[str]:
|
|
75
|
+
"""The asset's default connection name."""
|
|
76
|
+
return _env("BRUIN_CONNECTION")
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def run_id(self) -> Optional[str]:
|
|
80
|
+
return _env("BRUIN_RUN_ID")
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def is_full_refresh(self) -> bool:
|
|
84
|
+
value = (_env("BRUIN_FULL_REFRESH") or "").lower()
|
|
85
|
+
return value in ("1", "true", "yes")
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def vars(self) -> "dict[str, Any]":
|
|
89
|
+
"""Pipeline variables, types preserved from their JSON schema."""
|
|
90
|
+
raw = _env("BRUIN_VARS")
|
|
91
|
+
if not raw:
|
|
92
|
+
return {}
|
|
93
|
+
try:
|
|
94
|
+
parsed = json.loads(raw)
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
return {}
|
|
97
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
98
|
+
|
|
99
|
+
def __repr__(self) -> str: # pragma: no cover - debugging nicety
|
|
100
|
+
return (
|
|
101
|
+
f"renart.context(pipeline={self.pipeline!r}, asset={self.asset_name!r}, "
|
|
102
|
+
f"start_date={self.start_date!r}, end_date={self.end_date!r}, "
|
|
103
|
+
f"run_id={self.run_id!r}, is_full_refresh={self.is_full_refresh!r})"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
context = _Context()
|
renart/context.pyi
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
class _Context:
|
|
5
|
+
@property
|
|
6
|
+
def start_date(self) -> date | None: ...
|
|
7
|
+
@property
|
|
8
|
+
def start_datetime(self) -> datetime | None: ...
|
|
9
|
+
@property
|
|
10
|
+
def end_date(self) -> date | None: ...
|
|
11
|
+
@property
|
|
12
|
+
def end_datetime(self) -> datetime | None: ...
|
|
13
|
+
@property
|
|
14
|
+
def execution_date(self) -> datetime | None: ...
|
|
15
|
+
@property
|
|
16
|
+
def pipeline(self) -> str | None: ...
|
|
17
|
+
@property
|
|
18
|
+
def asset_name(self) -> str | None: ...
|
|
19
|
+
@property
|
|
20
|
+
def connection(self) -> str | None: ...
|
|
21
|
+
@property
|
|
22
|
+
def run_id(self) -> str | None: ...
|
|
23
|
+
@property
|
|
24
|
+
def is_full_refresh(self) -> bool: ...
|
|
25
|
+
@property
|
|
26
|
+
def vars(self) -> dict[str, Any]: ...
|
|
27
|
+
|
|
28
|
+
context: _Context
|
renart/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: renart
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Renart SDK for Python assets: query project data through the renart runner.
|
|
5
|
+
Home-page: https://getrenart.com
|
|
6
|
+
Project-URL: Documentation, https://getrenart.com/docs/asset-types/python-assets/
|
|
7
|
+
Project-URL: Source, https://github.com/renart-data/renart
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Requires-Dist: pyarrow>=15.0.0
|
|
11
|
+
Requires-Dist: pandas>=1.5
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Renart Python SDK
|
|
15
|
+
|
|
16
|
+
renart provides the Python package used by Python assets and Python
|
|
17
|
+
notebook cells in Renart. Renart injects its matching SDK wheel automatically
|
|
18
|
+
at runtime; the PyPI distribution is available for type checking, editors, and
|
|
19
|
+
CI environments that inspect project code outside the Renart process.
|
|
20
|
+
|
|
21
|
+
query() connects to a token-scoped broker started by the Renart runner. It
|
|
22
|
+
does not contain database credentials and is not a standalone database client.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
renart/__init__.py,sha256=i3gPwcZoHzzkoSaqyvlOrTrI42BcPvLGdCIS6EqZ6DQ,691
|
|
2
|
+
renart/__init__.pyi,sha256=1Drc_oITmRq35yYrwr3fJ1ir-aVE2PRfPEb9K5ERHKk,261
|
|
3
|
+
renart/_client.py,sha256=goGCSSxqJRdnIR_3vCS5r5hVOdq_6OE6Qc-av9CNaC4,4387
|
|
4
|
+
renart/_client.pyi,sha256=s0QhbqBlRstNPgNNfXZL3qnXK0VxRa61r5_Np_sSEVM,368
|
|
5
|
+
renart/context.py,sha256=lLBf_khFT2CEfWPPtZJJYlfd3lU0Q5HvslU4wzsVkUs,3027
|
|
6
|
+
renart/context.pyi,sha256=ZUeog2ZUES8lI2XvnZMsnNQIshHYyK4UA02waNvWnpY,749
|
|
7
|
+
renart/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
|
+
renart-0.3.2.dist-info/METADATA,sha256=0Zf5LfhZR-Z_TWzjFBUvWR1V6tlhUrBQlBPrsfBUGsQ,922
|
|
9
|
+
renart-0.3.2.dist-info/WHEEL,sha256=iAQXzXWZKPhmEBuSL_3wqpOatQ9An7mT6wPP1Py91F0,77
|
|
10
|
+
renart-0.3.2.dist-info/top_level.txt,sha256=AOLaNFL0wxJOm6mSttggaCfHcZecQznwf8R3AnyzxM4,7
|
|
11
|
+
renart-0.3.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
renart
|