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
2 changes: 1 addition & 1 deletion .github/workflows/install-dependencies-and-run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FWPSemantix%2Ftimbr_python_http.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FWPSemantix%2Ftimbr_python_http?ref=badge_shield&issueType=license)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FWPSemantix%2Ftimbr_python_http.svg?type=shield&issueType=security)](https://app.fossa.com/projects/git%2Bgithub.com%2FWPSemantix%2Ftimbr_python_http?ref=badge_shield&issueType=security)

[![Python 3.9](https://img.shields.io/badge/python-3.9-blue)](https://www.python.org/downloads/release/python-3921/)
[![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-31017/)
[![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-31112/)
[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3129/)
Expand All @@ -15,7 +14,7 @@ This project is a pure python connector to timbr (no dependencies required).

## Dependencies
- Access to a timbr-server
- Python from 3.9.13 or newer
- Python from 3.10 or newer

## Installation
- Install as clone repository:
Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pytimbr_api"
version = "2.0.0"
version = "2.1.0"
description = "Timbr REST API connector"
readme = "README.md"
license = "MIT"
Expand Down Expand Up @@ -34,12 +34,11 @@ classifiers = [
"Intended Audience :: Developers",
"Topic :: Software Development :: Build Tools",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12"
]
requires-python = ">=3.9"
requires-python = ">=3.10"
dependencies = [
"requests>=2.32.4"
]
Expand Down
4 changes: 2 additions & 2 deletions pytimbr_api/timbr_http_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def run_query(
url: str,
ontology: str,
token: str,
query: str,
query: str | bytes,
datasource: str = None,
nested: str = 'false',
verify_ssl: bool = True,
Expand Down Expand Up @@ -66,7 +66,7 @@ def run_query(
response = requests.post(
f'{base_url}timbr/openapi/ontology/{ontology}/query{datasource_addition}',
headers = headers,
data = query,
data = query.encode('utf-8') if isinstance(query, str) else query,
Comment thread
semantiDan marked this conversation as resolved.
verify = verify_ssl,
)
return _parse_response(response)
Expand Down
Binary file modified requirements.txt
Binary file not shown.
1 change: 1 addition & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ def test_config():
"jwt_password": os.getenv("JWT_PASSWORD"),
"jwt_scope": os.getenv("JWT_SCOPE"),
"jwt_secret": os.getenv("JWT_SECRET"),
"timbr_user_password": os.getenv("TIMBR_USER_PASSWORD"),
}
78 changes: 78 additions & 0 deletions test/test_encoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import pytest
from pytimbr_api.timbr_http_connector import run_query


def test_run_query_with_unicode_characters(test_config):
"""Test that queries with unicode characters are properly encoded to UTF-8."""
url = test_config['url']
ontology = test_config['ontology']
token = test_config['token']

# Query with unicode characters - using simple SELECT with unicode strings
# This tests that UTF-8 encoding works without requiring specific tables
query_with_unicode = "SELECT '测试' as chinese, 'café' as french, '😀' as emoji"
Comment thread
semantiDan marked this conversation as resolved.

results = run_query(url, ontology, token, query_with_unicode)

assert results is not None, "Results should not be None for unicode query"
assert len(results) > 0, "Results should contain data"
# Verify unicode characters are correctly processed in results
first_row = results[0]
assert first_row['chinese'] == '测试', "Chinese characters should be preserved"
assert first_row['french'] == 'café', "French accented characters should be preserved"
assert first_row['emoji'] == '😀', "Emoji should be preserved"


def test_run_query_with_already_encoded_bytes(test_config):
"""Test that queries already encoded as bytes are handled correctly."""
url = test_config['url']
ontology = test_config['ontology']
token = test_config['token']

# Query already encoded as bytes
query_as_bytes = b"SELECT 1"

results = run_query(url, ontology, token, query_as_bytes)

assert results is not None, "Results should not be None for bytes query"
assert len(results) > 0, "Results should contain data"


def test_run_query_with_special_sql_characters(test_config):
"""Test that queries with special SQL characters are properly encoded."""
url = test_config['url']
ontology = test_config['ontology']
token = test_config['token']

# Query with special characters that need proper encoding
query_with_special_chars = "SELECT 'O''Brien' as name, '©' as copyright"
Comment thread
semantiDan marked this conversation as resolved.

results = run_query(url, ontology, token, query_with_special_chars)

assert results is not None, "Results should not be None for query with special characters"
assert len(results) > 0, "Results should contain data"
# Verify special characters are correctly processed in results
first_row = results[0]
assert first_row['name'] == "O'Brien", "Apostrophe in name should be preserved"
assert first_row['copyright'] == '©', "Copyright symbol should be preserved"


def test_run_query_with_multilingual_text(test_config):
"""Test that queries with multiple languages are properly encoded."""
url = test_config['url']
ontology = test_config['ontology']
token = test_config['token']

# Query with multiple languages (Latin, Cyrillic, Arabic, Japanese)
query_multilingual = "SELECT 'Hello' as english, 'Привет' as russian, 'مرحبا' as arabic, 'こんにちは' as japanese"
Comment thread
semantiDan marked this conversation as resolved.

results = run_query(url, ontology, token, query_multilingual)

assert results is not None, "Results should not be None for multilingual query"
assert len(results) > 0, "Results should contain data"
# Verify multilingual characters are correctly processed in results
first_row = results[0]
assert first_row['english'] == 'Hello', "English text should be preserved"
assert first_row['russian'] == 'Привет', "Russian Cyrillic text should be preserved"
assert first_row['arabic'] == 'مرحبا', "Arabic text should be preserved"
assert first_row['japanese'] == 'こんにちは', "Japanese text should be preserved"
24 changes: 13 additions & 11 deletions test/test_user_impersonation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
from pytimbr_api.timbr_http_connector import run_query
import uuid

create_granting_user_stmt = "CREATE USER {username} OPTIONS(email='{username}@timbr-test.ai', password='{password}', first_name='{first_name}', last_name='{last_name}');" \
create_granting_user_stmt = "CREATE USER {username} PASSWORD '{password}' OPTIONS(email='{username}@timbr-test.ai', first_name='{first_name}', last_name='{last_name}');" \
"GRANT QUERY ON ALL DATASOURCE TO USER {username};" \
"GRANT ACCESS ON ALL ONTOLOGY TO USER {username};" \
"GRANT EDIT ON ALL USER TO USER {username};"

create_impersonating_user_stmt = "CREATE USER {username} OPTIONS(email='{username}@timbr-test.ai', password='{password}', first_name='{first_name}', last_name='{last_name}');" \
create_impersonating_user_stmt = "CREATE USER {username} PASSWORD '{password}' OPTIONS(email='{username}@timbr-test.ai', first_name='{first_name}', last_name='{last_name}');" \
"GRANT ACCESS ON ALL ONTOLOGY TO USER {username};" \
"GRANT QUERY ON ALL USER TO USER {username};"

Expand All @@ -24,13 +24,15 @@

# Generate unique suffix using timestamp and UUID
unique_suffix = str(uuid.uuid4())[:8]
granting_user = f"timbr_python_http_granting_user_{unique_suffix}"
impersonating_user = f"timbr_python_http_impersonating_user_{unique_suffix}"
# granting_user = f"timbr_granting_{unique_suffix}"
# impersonating_user = f"timbr_impersonating_{unique_suffix}"
granting_user = f"timbr_granting"
impersonating_user = f"timbr_impersonating"

def create_users(test_config):
print("Creating users...")
granting_user_stmt = create_granting_user_stmt.format(username=granting_user, password="SecurePassword123", first_name="Granting", last_name="User")
impersonating_user_stmt = create_impersonating_user_stmt.format(username=impersonating_user, password="SecurePassword123", first_name="Impersonating", last_name="User")
granting_user_stmt = create_granting_user_stmt.format(username=granting_user, password=test_config['timbr_user_password'], first_name="Granting", last_name="User")
impersonating_user_stmt = create_impersonating_user_stmt.format(username=impersonating_user, password=test_config['timbr_user_password'], first_name="Impersonating", last_name="User")

run_query(
url=test_config['url'],
Expand Down Expand Up @@ -120,15 +122,15 @@ def drop_users(test_config):
def setup_test_users(test_config):
"""Fixture to create users at the start and drop them at the end"""
# Setup: Create users
create_users(test_config)
# create_users(test_config)

yield test_config # This provides the test_config to the test

# Teardown: Drop users (runs even if tests fail)
try:
drop_users(test_config)
except Exception as e:
print(f"Warning: Failed to drop users during teardown: {e}")
# try:
# drop_users(test_config)
# except Exception as e:
# print(f"Warning: Failed to drop users during teardown: {e}")

class TestUserImpersonation:
def test_user_impersonation(self, setup_test_users):
Expand Down