relay-metis 0.1.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,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ timeout-minutes: 10
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v5
21
+ with:
22
+ enable-cache: true
23
+ cache-dependency-glob: "uv.lock"
24
+
25
+ - name: Set up Python ${{ matrix.python-version }}
26
+ run: uv python install ${{ matrix.python-version }}
27
+
28
+ - name: Install dependencies
29
+ run: uv sync --python ${{ matrix.python-version }}
30
+
31
+ - name: Ruff format
32
+ run: uv run ruff format --check --diff .
33
+
34
+ - name: Ruff check
35
+ run: uv run ruff check .
36
+
37
+ - name: Tests
38
+ run: uv run pytest tests/ -v
39
+
40
+ - name: Build sanity check
41
+ run: uv build
@@ -0,0 +1,43 @@
1
+ # Publishes relay-metis to PyPI via trusted publishing (OIDC) — no stored token.
2
+ #
3
+ # One-off setup on PyPI before the first release: add a "pending trusted
4
+ # publisher" for project `relay-metis` with owner `relaytech-co`, repository
5
+ # `metis-python-client`, workflow `publish.yaml`, environment `pypi`. The
6
+ # first successful publish claims the project name atomically.
7
+ #
8
+ # Releasing = bump `version` in pyproject.toml, merge, then create a GitHub
9
+ # Release (tag `v<version>`); this workflow does the rest.
10
+ name: Publish to PyPI
11
+
12
+ on:
13
+ release:
14
+ types: [published]
15
+
16
+ jobs:
17
+ publish:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ environment: pypi
21
+ permissions:
22
+ id-token: write
23
+ contents: read
24
+
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v5
30
+
31
+ - name: Build wheel and sdist
32
+ run: uv build
33
+
34
+ - name: Check tag matches package version
35
+ run: |
36
+ version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
37
+ if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
38
+ echo "Release tag ${GITHUB_REF_NAME} does not match pyproject version v${version}" >&2
39
+ exit 1
40
+ fi
41
+
42
+ - name: Publish
43
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ dist/
5
+ .cursor/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Relay Technologies
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,108 @@
1
+ Metadata-Version: 2.5
2
+ Name: relay-metis
3
+ Version: 0.1.0
4
+ Summary: Lightweight Python client for Metis, Relay's Cube-backed metrics layer.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: pandas>=2.0
9
+ Requires-Dist: requests>=2.31
10
+ Description-Content-Type: text/markdown
11
+
12
+ # metis
13
+
14
+ Lightweight Python client for Metis, Relay's Cube-backed metrics layer. Queries a Metis
15
+ view and returns a typed, annotated pandas DataFrame — the metrics you see in dashboards,
16
+ in a notebook, computed the same way.
17
+
18
+ ## Setup
19
+
20
+ ```bash
21
+ export METIS_API_URL="https://<deployment>.cubecloudapp.dev" # /cubejs-api/v1 optional
22
+ export METIS_API_KEY="<pre-minted Cube API token>"
23
+ ```
24
+
25
+ Both can also be passed explicitly: `MetisClient(url=..., api_key=...)` (constructor
26
+ arguments win over the environment).
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from datetime import date
32
+ from metis import MetisClient
33
+
34
+ client = MetisClient()
35
+
36
+ df = client.query(
37
+ "orders",
38
+ measures=["order_count", "revenue_sum"],
39
+ dimensions=["status"],
40
+ time_dimension="created_at",
41
+ granularity="day",
42
+ date_range=(date(2026, 8, 1), date(2026, 8, 31)),
43
+ filters={"is_priority": True},
44
+ order={"created_at": "asc"},
45
+ )
46
+ ```
47
+
48
+ Member names are unprefixed and scoped to the view; result columns come back
49
+ prefix-stripped. Discovery:
50
+
51
+ ```python
52
+ client.meta() # one row per view/member: kind, type, title, description
53
+ ```
54
+
55
+ Unknown views/members fail before the query is sent, with did-you-mean suggestions.
56
+
57
+ ### Filters
58
+
59
+ The mapping form is sugar for equals/in (`None` means "is not set"):
60
+
61
+ ```python
62
+ filters = {"status": ["shipped", "delivered"], "is_priority": True}
63
+ ```
64
+
65
+ Anything richer takes raw Cube filter dicts (member names still unprefixed):
66
+
67
+ ```python
68
+ filters = [{"member": "revenue_sum", "operator": "gt", "values": ["1000"]}]
69
+ ```
70
+
71
+ ### Dtypes
72
+
73
+ Model-driven, never inferred from whichever values a result happens to contain:
74
+
75
+ | Member | dtype |
76
+ | --- | --- |
77
+ | count / count_distinct measures | `Int64` (nullable) |
78
+ | all other numeric measures | `float64` |
79
+ | numeric dimensions, integer-typed at source | `Int64` (nullable) |
80
+ | numeric dimensions, decimal-typed at source | `float64` |
81
+ | booleans | `boolean` (nullable) |
82
+ | time members | `datetime64` |
83
+
84
+ Numeric dimensions are classified from the raw serialisation: integer columns always
85
+ arrive as bare digit strings and classify correctly. Known caveat: a decimal-typed
86
+ dimension (FLOAT64 or NUMERIC) whose result values are all whole also arrives as bare
87
+ digits and reads as `Int64` until a decimal value appears. Override any column
88
+ per-call: `dtypes={"weight_grams": "float64"}`.
89
+
90
+ Query provenance (payload, Cube annotation, request time) is attached under
91
+ `df.attrs["metis"]`.
92
+
93
+ ### Escape hatch
94
+
95
+ ```python
96
+ client.load(
97
+ {"query": {...}, "cache": "must-revalidate"}
98
+ ) # raw Cube REST payload -> raw dict
99
+ ```
100
+
101
+ ## Behaviour notes
102
+
103
+ - Cache mode is hard-coded to `must-revalidate`: serves cached data while current,
104
+ never stale data.
105
+ - Long-running queries are polled (Cube "Continue wait") within a wall-clock budget of
106
+ 120s by default — raise `total_wait_budget_seconds` for heavy queries.
107
+ - Transient errors retry up to 3 times with backoff; auth failures and query errors
108
+ fail immediately and loudly.
@@ -0,0 +1,97 @@
1
+ # metis
2
+
3
+ Lightweight Python client for Metis, Relay's Cube-backed metrics layer. Queries a Metis
4
+ view and returns a typed, annotated pandas DataFrame — the metrics you see in dashboards,
5
+ in a notebook, computed the same way.
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ export METIS_API_URL="https://<deployment>.cubecloudapp.dev" # /cubejs-api/v1 optional
11
+ export METIS_API_KEY="<pre-minted Cube API token>"
12
+ ```
13
+
14
+ Both can also be passed explicitly: `MetisClient(url=..., api_key=...)` (constructor
15
+ arguments win over the environment).
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from datetime import date
21
+ from metis import MetisClient
22
+
23
+ client = MetisClient()
24
+
25
+ df = client.query(
26
+ "orders",
27
+ measures=["order_count", "revenue_sum"],
28
+ dimensions=["status"],
29
+ time_dimension="created_at",
30
+ granularity="day",
31
+ date_range=(date(2026, 8, 1), date(2026, 8, 31)),
32
+ filters={"is_priority": True},
33
+ order={"created_at": "asc"},
34
+ )
35
+ ```
36
+
37
+ Member names are unprefixed and scoped to the view; result columns come back
38
+ prefix-stripped. Discovery:
39
+
40
+ ```python
41
+ client.meta() # one row per view/member: kind, type, title, description
42
+ ```
43
+
44
+ Unknown views/members fail before the query is sent, with did-you-mean suggestions.
45
+
46
+ ### Filters
47
+
48
+ The mapping form is sugar for equals/in (`None` means "is not set"):
49
+
50
+ ```python
51
+ filters = {"status": ["shipped", "delivered"], "is_priority": True}
52
+ ```
53
+
54
+ Anything richer takes raw Cube filter dicts (member names still unprefixed):
55
+
56
+ ```python
57
+ filters = [{"member": "revenue_sum", "operator": "gt", "values": ["1000"]}]
58
+ ```
59
+
60
+ ### Dtypes
61
+
62
+ Model-driven, never inferred from whichever values a result happens to contain:
63
+
64
+ | Member | dtype |
65
+ | --- | --- |
66
+ | count / count_distinct measures | `Int64` (nullable) |
67
+ | all other numeric measures | `float64` |
68
+ | numeric dimensions, integer-typed at source | `Int64` (nullable) |
69
+ | numeric dimensions, decimal-typed at source | `float64` |
70
+ | booleans | `boolean` (nullable) |
71
+ | time members | `datetime64` |
72
+
73
+ Numeric dimensions are classified from the raw serialisation: integer columns always
74
+ arrive as bare digit strings and classify correctly. Known caveat: a decimal-typed
75
+ dimension (FLOAT64 or NUMERIC) whose result values are all whole also arrives as bare
76
+ digits and reads as `Int64` until a decimal value appears. Override any column
77
+ per-call: `dtypes={"weight_grams": "float64"}`.
78
+
79
+ Query provenance (payload, Cube annotation, request time) is attached under
80
+ `df.attrs["metis"]`.
81
+
82
+ ### Escape hatch
83
+
84
+ ```python
85
+ client.load(
86
+ {"query": {...}, "cache": "must-revalidate"}
87
+ ) # raw Cube REST payload -> raw dict
88
+ ```
89
+
90
+ ## Behaviour notes
91
+
92
+ - Cache mode is hard-coded to `must-revalidate`: serves cached data while current,
93
+ never stale data.
94
+ - Long-running queries are polled (Cube "Continue wait") within a wall-clock budget of
95
+ 120s by default — raise `total_wait_budget_seconds` for heavy queries.
96
+ - Transient errors retry up to 3 times with backoff; auth failures and query errors
97
+ fail immediately and loudly.
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "relay-metis"
3
+ version = "0.1.0"
4
+ description = "Lightweight Python client for Metis, Relay's Cube-backed metrics layer."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "pandas>=2.0",
11
+ "requests>=2.31",
12
+ ]
13
+
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/metis"]
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=8.0",
24
+ "ruff>=0.16.6",
25
+ ]
26
+
27
+ [tool.ruff]
28
+ target-version = "py311"
29
+ src = ["src", "tests"]
@@ -0,0 +1,5 @@
1
+ from metis.client import MetisClient as MetisClient
2
+ from metis.client import MetisConfigError as MetisConfigError
3
+ from metis.client import MetisError as MetisError
4
+ from metis.client import MetisQueryError as MetisQueryError
5
+ from metis.client import MetisTimeoutError as MetisTimeoutError