Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ This project is a pure python connector to timbr (no dependencies required).
is_jwt = <True/False>,
jwt_tenant_id = "<JWT_TENANT_ID>",
additional_headers = <{ "x-api-impersonate-user": "<user to impersonate>" }>,
is_async = <True/False>,
)

# url - Required - String - The IP / Hostname of the Timbr platform.
Expand All @@ -56,10 +57,57 @@ This project is a pure python connector to timbr (no dependencies required).
# verify_ssl - Optional - Boolean - Verifying the target server's SSL Certificate, use False to disable this process.
# enable_IPv6 - Optional - Boolean - Change to 'true' if you are using IPv6 connection.
# is_jwt - Optional - Boolean - Set to True if you are using JWT token, otherwise set to False.
# jwt_tenant_id - Optional - String - The tenant ID for JWT authentication
# jwt_tenant_id - Optional - String - The tenant ID for JWT authentication.
# additional_headers - Optional - Dict - Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
# is_async - Optional - Boolean - Set to True to submit the query asynchronously. Returns the submission response (including response_id) immediately instead of waiting for the result. Use get_async_result() to poll for completion. Default: False.
```

## Async query execution

For long-running queries that may time out on a synchronous HTTP connection, use the two-step async flow:

1. **Submit** — call `run_query` with `is_async=True`. It returns immediately with a submission response containing a `response_id`.
2. **Poll** — call `get_async_result` with the `response_id` to check the status. Repeat until `status` is `'completed'` or `'error'`.

```python
import time
import pytimbr_api

URL = "https://mytimbrenv.com:443"
ONTOLOGY = "my_ontology"
TOKEN = "tk_mytimbrtoken"

# Step 1: submit the query asynchronously
submission = pytimbr_api.run_query(
url = URL,
ontology = ONTOLOGY,
token = TOKEN,
query = "SELECT * FROM timbr.large_table",
is_async = True,
)
response_id = submission["response_id"]
print(f"Query submitted — response_id: {response_id}")

# Step 2: poll until the result is ready
while True:
result = pytimbr_api.get_async_result(
url = URL,
response_id = response_id,
token = TOKEN,
)
status = result["status"]
if status == "completed":
print(result["response"])
break
elif status == "error":
raise Exception(f"Query failed: {result.get('error')}")
else:
print(f"Status: {status} — waiting...")
time.sleep(3)
```

`get_async_result` makes a **single** request each call and returns the raw server response — polling cadence and timeout logic are left to the caller.

### Using Timbr token

#### HTTP example
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ classifiers = [
]
requires-python = ">=3.10"
dependencies = [
"requests>=2.32.4"
"requests>=2.33.0"
]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion pytimbr_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .timbr_http_connector import run_query, advancedQueryExecute, simpleQueryExecution, executeQuery
from .timbr_http_connector import run_query, get_async_result, advancedQueryExecute, simpleQueryExecution, executeQuery
121 changes: 97 additions & 24 deletions pytimbr_api/timbr_http_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ def _parse_response(response):
return response_json


def _build_headers(
token: str,
nested: str = 'false',
is_jwt: bool = False,
jwt_tenant_id: str = None,
additional_headers: dict = None,
) -> dict:
headers = {
'Content-Type': 'application/text',
'nested': nested,
'Connection': 'close',
}
if is_jwt:
headers['x-jwt-token'] = token
if jwt_tenant_id:
headers['x-jwt-tenant-id'] = jwt_tenant_id
else:
headers['x-api-key'] = token
if additional_headers:
for key, value in additional_headers.items():
headers[key.replace('_', '-')] = value
return headers


def run_query(
url: str,
ontology: str,
Expand All @@ -36,40 +60,89 @@ def run_query(
is_jwt: bool = False,
jwt_tenant_id: str = None,
additional_headers: dict = None,
is_async: bool = False,
):
datasource_addition = ''
if datasource:
datasource_addition = f'?datasource={datasource}'
datasource_addition = f'?datasource={datasource}'

base_url = url
if not base_url.endswith('/'):
base_url = base_url + '/'

headers = {
'Content-Type': 'application/text',
'nested': nested,
'Connection': 'close',
}
base_url = base_url + '/'

if is_jwt:
headers['x-jwt-token'] = token
if jwt_tenant_id:
headers['x-jwt-tenant-id'] = jwt_tenant_id
else:
headers['x-api-key'] = token

if additional_headers:
for key, value in additional_headers.items():
headers[key.replace('_', '-')] = value
headers = _build_headers(token, nested, is_jwt, jwt_tenant_id, additional_headers)
if is_async:
headers['x-async'] = 'true'

requests.packages.urllib3.util.connection.HAS_IPV6 = enable_IPv6
response = requests.post(
f'{base_url}timbr/openapi/ontology/{ontology}/query{datasource_addition}',
headers = headers,
data = query.encode('utf-8') if isinstance(query, str) else query,
verify = verify_ssl,
f'{base_url}timbr/openapi/ontology/{ontology}/query{datasource_addition}',
headers=headers,
data=query.encode('utf-8') if isinstance(query, str) else query,
verify=verify_ssl,
)
return _parse_response(response)

if not is_async:
return _parse_response(response)

# Async mode: expect 202 with response_id, return submission body as-is
if response.status_code != 202:
raise Exception(f'Error submitting async query: {response.text}')
try:
submission = response.json()
except Exception as e:
raise Exception(f'Could not parse async submission response: {e}')

if not submission.get('response_id'):
raise Exception(f'Server did not return a response_id: {submission}')

return submission


def get_async_result(
url: str,
response_id: str,
token: str,
is_jwt: bool = False,
jwt_tenant_id: str = None,
verify_ssl: bool = True,
additional_headers: dict = None,
) -> dict:
"""Fetch the current result of a previously submitted async query.

Makes a single GET request and returns the raw JSON response from the server.
The caller is responsible for checking ``result['status']`` and polling again
if it is ``'running'``.

Returns a dict such as::

{'status': 'running', 'response_id': '...', 'message': '...'}
{'status': 'completed', 'response': [...], 'original_status_code': 200}
{'status': 'error', 'error': '...', 'error_type': '...'}

Raises:
Exception: if the server returns 404 (result expired/not found) or a non-200 HTTP status.
"""
base_url = url
if not base_url.endswith('/'):
base_url = base_url + '/'

headers = _build_headers(token, is_jwt=is_jwt, jwt_tenant_id=jwt_tenant_id,
additional_headers=additional_headers)

response = requests.get(
f'{base_url}timbr/openapi/get-async-results/{response_id}',
headers=headers,
verify=verify_ssl,
)
if response.status_code == 404:
raise Exception(f'Async result not found (expired or already fetched): {response_id}')
if response.status_code != 200:
raise Exception(f'Error fetching async result: {response.text}')
try:
return response.json()
except Exception as e:
raise Exception(f'Could not parse async result response: {e}')


# Deprecated - Backward compatibility
Expand Down
Binary file modified requirements.txt
Binary file not shown.
Loading
Loading