Skip to content

Commit 2058aa8

Browse files
authored
Merge pull request #1 from NarrativeScience/init
QPT-35790 Initial commit
2 parents 76ad2f1 + cee3bfc commit 2058aa8

File tree

17 files changed

+1277
-61
lines changed

17 files changed

+1277
-61
lines changed

.circleci/config.yml

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,42 @@
11
version: 2.1
22

33
orbs:
4-
ghpr: narrativescience/[email protected]
5-
6-
commands:
7-
install-deps:
8-
description: Install dependencies and initialize pre-commit hooks
9-
steps:
10-
- run:
11-
command: |
12-
pip install pre-commit tox flit
13-
pre-commit install
14-
name: Install dependencies
4+
ghpr: narrativescience/[email protected]
155

166
jobs:
177
test:
188
docker:
19-
- image: circleci/python:3.7
9+
- image: circleci/python:3.6
2010
steps:
2111
- ghpr/build-prospective-branch
22-
- install-deps
12+
- run:
13+
name: Install dependencies
14+
command: |
15+
pip install poetry
16+
poetry install
17+
poetry run pre-commit install
2318
- run:
2419
name: Run commit hooks
2520
command: |
26-
pre-commit run \
21+
poetry run pre-commit run \
2722
--source "origin/${GITHUB_PR_BASE_BRANCH}" \
2823
--origin "origin/${CIRCLE_BRANCH}" \
2924
--show-diff-on-failure
30-
- run: tox
31-
- run: flit build
25+
- run: poetry build
26+
- run: poetry run pytest
3227
- ghpr/post-pr-comment:
3328
comment: Tests failed!
3429
when: on_fail
3530
publish:
3631
docker:
37-
- image: circleci/python:3.7
32+
- image: circleci/python:3.6
3833
steps:
3934
- checkout
40-
- run: pip install flit
41-
- run: flit build
42-
- run: flit publish
35+
- run: pip install poetry
36+
- run: poetry install
37+
- run: poetry build
38+
- run: poetry config pypi-token.pypi "$POETRY_PYPI_TOKEN_PYPI"
39+
- run: poetry publish
4340

4441
workflows:
4542
pull_request:

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ignore = E203,E501,E731,W503,W605
88
import-order-style = google
99
# Packages added in this list should be added to the setup.cfg file as well
1010
application-import-names =
11-
mypackagename
11+
cubejsclient
1212
exclude =
1313
*vendor*
1414
.venv

README.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
1-
# my-package-name
1+
# cubejsclient
22

3-
[![](https://img.shields.io/pypi/v/my-package-name.svg)](https://pypi.org/pypi/my-package-name/) [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
3+
[![](https://img.shields.io/pypi/v/cubejsclient.svg)](https://pypi.org/pypi/cubejsclient/) [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
44

5-
<!-- Short description of the package -->
5+
Async Python Cube.js client
66

77
Features:
88

9-
- <!-- list of features -->
9+
- Cube.js API client that makes async requests
10+
- Rich objects for building queries with measures, dimensions, etc.
1011

1112
Table of Contents:
1213

1314
- [Installation](#installation)
14-
- [Guide](#guide)
1515
- [Development](#development)
1616

1717
## Installation
1818

19-
my-package-name requires Python 3.6 or above.
19+
cubejsclient requires Python 3.6 or above.
2020

2121
```bash
22-
pip install mypackagename
22+
pip install cubejsclient
2323
```
2424

25-
## Guide
26-
27-
<!-- Subsections explaining how to use the package -->
28-
2925
## Development
3026

31-
To develop my-package-name, install dependencies and enable the pre-commit hook:
27+
To develop cubejsclient, install dependencies and enable the pre-commit hook:
3228

3329
```bash
3430
pip install pre-commit poetry

cubejsclient/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Async Python Cube.js client"""
2+
3+
__version__ = "0.1.0"
4+
5+
from .client import *
6+
from .enums import *
7+
from .filters import *
8+
from .objects import *
9+
from .query import *

cubejsclient/client.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Contains the Cube.js API client"""
2+
3+
from datetime import datetime, timedelta
4+
from typing import Any, Dict, Optional
5+
6+
import backoff
7+
import httpx
8+
import jwt
9+
10+
from .query import Query
11+
12+
13+
class CubeClient:
14+
"""Cube.js API client"""
15+
16+
def __init__(
17+
self,
18+
host: str = "http://localhost:4000",
19+
base_path: str = "/cubejs-api",
20+
secret: Optional[str] = None,
21+
load_request_timeout: float = 30.0,
22+
token_ttl_hours: int = 1,
23+
) -> None:
24+
"""Initializer
25+
26+
Args:
27+
host: Cube.js API host
28+
base_path: Cube.js API base path
29+
secret: Secret for signing tokens. Set to None to skip authentication.
30+
load_request_timeout: Timeout in seconds to wait for load responses
31+
token_ttl_hours: TTL in hours for the token lifetime
32+
33+
"""
34+
self._secret = secret
35+
self._load_request_timeout = load_request_timeout
36+
self._token_ttl_hours = token_ttl_hours
37+
self._http_client = httpx.AsyncClient(
38+
base_url=f"{host.rstrip('/')}/{base_path.strip('/')}"
39+
)
40+
41+
self._token = None
42+
43+
def _get_signed_token(self) -> Optional[str]:
44+
"""Get or refresh the authentication token
45+
46+
Returns:
47+
token or None if no secret was configured
48+
49+
"""
50+
if not self._secret:
51+
return None
52+
53+
now = datetime.now()
54+
if not self._token or self._token_expiration <= now:
55+
self._token_expiration = now + timedelta(hours=self._token_ttl_hours)
56+
self._token = jwt.encode(
57+
{"exp": self._token_expiration}, self._secret, algorithm="HS256"
58+
)
59+
60+
return self._token
61+
62+
@property
63+
def token(self) -> Optional[str]:
64+
"""Alias for getting the current token value"""
65+
return self._get_signed_token()
66+
67+
async def load(self, query: Query) -> Dict[str, Any]:
68+
"""Get the data for a query.
69+
70+
Args:
71+
query: Query object
72+
73+
Returns:
74+
dict with properties:
75+
* query -- The query passed via params
76+
* data -- Formatted dataset of query results
77+
* annotation -- Metadata for query. Contains descriptions for all query
78+
items.
79+
* title -- Human readable title from data schema.
80+
* shortTitle -- Short title for visualization usage (ex. chart overlay)
81+
* type -- Data type
82+
83+
"""
84+
return await self._request(
85+
"post",
86+
"/v1/load",
87+
body={"query": query.serialize()},
88+
timeout=self._load_request_timeout,
89+
)
90+
91+
@backoff.on_exception(
92+
backoff.expo, httpx.RequestError, max_tries=8, jitter=backoff.random_jitter
93+
)
94+
async def _request(
95+
self, method: str, path: str, body: Optional[Any] = None, timeout: float = 5.0
96+
):
97+
"""Make API request to Cube.js server
98+
99+
Args:
100+
method: HTTP method
101+
path: URL path
102+
body: Body to send with the request, if applicable
103+
timeout: Request timeout in seconds
104+
105+
Returns:
106+
response data
107+
108+
"""
109+
headers = {}
110+
if self.token:
111+
headers["Authorization"] = self.token
112+
113+
async with self._http_client as client:
114+
response = await client.request(
115+
method, path, json=body, headers=headers, timeout=timeout
116+
)
117+
response.raise_for_status()
118+
return response.json()

cubejsclient/enums.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Contains enums"""
2+
3+
import enum
4+
5+
6+
@enum.unique
7+
class TimeGranularity(enum.Enum):
8+
"""Time granularity"""
9+
10+
second = "second"
11+
minute = "minute"
12+
hour = "hour"
13+
day = "day"
14+
week = "week"
15+
month = "month"
16+
year = "year"
17+
null = None
18+
19+
20+
@enum.unique
21+
class Order(enum.Enum):
22+
"""Result ordering directions"""
23+
24+
asc = "asc"
25+
desc = "desc"
26+
27+
28+
@enum.unique
29+
class FilterOperator(enum.Enum):
30+
"""Operators used in filters"""
31+
32+
equals = "equals"
33+
not_equals = "notEquals"
34+
contains = "contains"
35+
not_contains = "notContains"
36+
gt = "gt"
37+
gte = "gte"
38+
lt = "lt"
39+
lte = "lte"
40+
is_set = "set"
41+
not_set = "notSet"
42+
in_date_range = "inDateRange"
43+
not_in_date_range = "notInDateRange"
44+
before_date = "beforeDate"
45+
after_date = "afterDate"

0 commit comments

Comments
 (0)