diff --git a/.github/workflows/install-dependencies-and-run-tests.yml b/.github/workflows/install-dependencies-and-run-tests.yml index 27651e2..2992742 100644 --- a/.github/workflows/install-dependencies-and-run-tests.yml +++ b/.github/workflows/install-dependencies-and-run-tests.yml @@ -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 }} diff --git a/README.md b/README.md index 5183bcb..e3e4355 100644 --- a/README.md +++ b/README.md @@ -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/) @@ -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: diff --git a/pyproject.toml b/pyproject.toml index c7f905a..c1a6f41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" @@ -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" ] diff --git a/pytimbr_api/timbr_http_connector.py b/pytimbr_api/timbr_http_connector.py index 60e3fda..b2c012e 100644 --- a/pytimbr_api/timbr_http_connector.py +++ b/pytimbr_api/timbr_http_connector.py @@ -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, @@ -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, verify = verify_ssl, ) return _parse_response(response) diff --git a/requirements.txt b/requirements.txt index b012b40..008df9a 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/test/conftest.py b/test/conftest.py index 93e8c23..3a21777 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -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"), } diff --git a/test/test_encoding.py b/test/test_encoding.py new file mode 100644 index 0000000..3b08802 --- /dev/null +++ b/test/test_encoding.py @@ -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" + + 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" + + 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" + + 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" diff --git a/test/test_user_impersonation.py b/test/test_user_impersonation.py index 1bbe53f..8f7d901 100644 --- a/test/test_user_impersonation.py +++ b/test/test_user_impersonation.py @@ -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};" @@ -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'], @@ -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):