feat: Initial version of CleverSwarm Python SDK
/ test (push) Successful in 9m10s

Extend CleverSwarm codebase with PDF and Machine and Worker IDs

ISSUES CLOSED: #1
This commit was merged in pull request #2.
This commit is contained in:
CoreRasurae
2025-09-28 22:21:10 +01:00
committed by Luis Mendes
parent 1aebcfda0f
commit cd808dc3dd
17 changed files with 219 additions and 66 deletions
+68
View File
@@ -0,0 +1,68 @@
on: [push]
env:
BUILD_DIR: "."
jobs:
test:
runs-on: docker
container:
image: modjular/modjular-python-testing:3.13
steps:
- name: Test Echo
run: echo 'Starting up the tests...'
- name: Check version and distribution
run: |
uname -srm
cat /etc/os-release
- name: Install pyenv and nodejs
run: |
sudo apt-get update
sudo apt-get install build-essential git nodejs npm -y
pyenv install 3.10.18 && pyenv local 3.10.18
pyenv install 3.11.13 && pyenv local 3.11.13
pyenv install 3.12.10 && pyenv local 3.12.10
pyenv install 3.13.5 && pyenv local 3.13.5
- name: Clone Repository
id: clone
uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-known-hosts: ${{ vars.SSH_KNOWN_HOSTS }}
- name: Install requirements
id: install
run: |
pyenv local 3.10.18
pip install -r requirements-dev.txt
pip install tox
pyenv local 3.11.13
pip install -r requirements-dev.txt
pip install tox
pyenv local 3.12.10
pip install -r requirements-dev.txt
pip install tox
pyenv local 3.13.5
pip install -r requirements-dev.txt
pip install tox
- name: Run the tests
run: |
pyenv local 3.10.18 3.11.13 3.12.10 3.13.5
tox -e lint,py310,py311,py312,py313,coverage -p all
- name: Upload coverage artifacts
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: coverage-reports
path: |
unit_tests_report.xml
unit_tests_report.html
coverage_report.xml
htmlcov
@@ -19,6 +19,7 @@ CleverSwarm - text to knowledge graph extraction benchmark client.
import argparse
import logging
import sys
from pathlib import Path
from typing import Dict, List, Set
@@ -221,6 +222,8 @@ class BenchmarkCLI(object):
print("Type: {}".format(job["type"]))
print("Status: {}".format(job["status"]))
print("Retries: {}".format(job["retries_count"]))
print("Machine ID: {}".format(job["machine_id"]))
print("Worker ID: {}".format(job["worker_id"]))
print("Input file type: {}".format((job["file_type"])))
if detailed:
print("Unstructured text input files:")
@@ -278,6 +281,15 @@ class BenchmarkCLI(object):
def main():
"""Main entry point for the benchmark client console script."""
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
parser = argparse.ArgumentParser(
description="CleverSwarm benchmark client for REST API."
)
@@ -19,6 +19,7 @@ CleverSwarm - unstructured text to knowledge graph triplets extractor client.
import argparse
import logging
import sys
from pathlib import Path
from typing import Any, Dict
@@ -187,6 +188,8 @@ class TextoToKGCLI(object):
print("Type: {}".format(job["type"]))
print("Status: {}".format(job["status"]))
print("Retries: {}".format(job["retries_count"]))
print("Machine ID: {}".format(job["machine_id"]))
print("Worker ID: {}".format(job["worker_id"]))
print("Input file type: {}".format((job["file_type"])))
if is_detailed:
print("Unstructured text input files:")
@@ -207,6 +210,15 @@ class TextoToKGCLI(object):
def main():
"""Main entry point for the text-to-KG client console script."""
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
parser = argparse.ArgumentParser(
description="CleverSwarm unstructured text to Knowledge Graph triplets client for REST API."
)
@@ -22,7 +22,7 @@ import os
import time
from getpass import getpass
from pathlib import Path
from typing import Any, Dict, List, Self
from typing import Any, Dict, List
import requests
@@ -143,7 +143,7 @@ class CleverSwarmClient(object):
)
retries += 1
def update_token(self, query_username: bool = False) -> Self:
def update_token(self, query_username: bool = False) -> "CleverSwarmClient":
"""
Updates the session authorization token.
:return: the BenchmarkClient instance
@@ -157,7 +157,6 @@ class CleverSwarmClient(object):
if self._username is None or query_username:
self._username = input("Please enter your username: ")
password = getpass("Please enter your password: ")
# TODO Hash password
data = {"username": self._username, "password": password}
r = requests.post(
@@ -1,14 +1,28 @@
from enum import StrEnum
import sys
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from enum import Enum
class StrEnum(str, Enum):
def _generate_next_value_(name, start, count, last_values):
return name
def __str__(self):
return self.value
class FileTypeAPI(StrEnum):
AutoDetect = "AutoDetect"
PDF = "PDF"
Markdown = "Markdown"
PlainText = "PlainText"
class FileType(StrEnum):
JSONL = "JSONL"
PDF = "PDF"
Markdown = "Markdown"
PlainText = "PlainText"
Unknown = "Unknown"
@@ -17,7 +17,19 @@ Module that contains the enumerations found when interfacing with Benchmark clie
limitations under the License.
"""
from enum import StrEnum
import sys
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from enum import Enum
class StrEnum(str, Enum):
def _generate_next_value_(name, start, count, last_values):
return name
def __str__(self):
return self.value
class JobType(StrEnum):
@@ -1,4 +1,16 @@
from enum import StrEnum
import sys
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from enum import Enum
class StrEnum(str, Enum):
def _generate_next_value_(name, start, count, last_values):
return name
def __str__(self):
return self.value
class ResponseTypeAPI(StrEnum):
+20 -23
View File
@@ -111,27 +111,6 @@ class TestCleverSwarmClient:
assert client._timeout == 30
assert client._triplets_template == "custom_{}_output.jsonl"
def test_client_file_operations_with_real_files(self):
"""Test client file operations with real files."""
with tempfile.TemporaryDirectory() as temp_dir:
# Create test files
text_file = Path(temp_dir) / "test.txt"
json_file = Path(temp_dir) / "test.json"
owl_file = Path(temp_dir) / "test.owl"
text_file.write_text("Test content")
json_file.write_text('{"test": "data"}')
owl_file.write_text('<rdf:RDF>test</rdf:RDF>')
# Test that files exist and can be used
assert text_file.exists()
assert json_file.exists()
assert owl_file.exists()
# Test that we can create a client and it recognizes the files
client = CleverSwarmClient("http://localhost:8000/api/v0/")
assert client._base_url == "http://localhost:8000/api/v0/"
def test_file_validation_with_missing_files(self):
"""Test file validation with missing files."""
# Test with non-existent files - this should raise InvalidFileException before calling update_token
@@ -157,10 +136,27 @@ class TestCleverSwarmClient:
json_file.write_text('{"test": "data"}')
jsonl_file.write_text('{"test": "data"}')
# These should not raise exceptions
# Test that files exist and are valid files
assert text_file.exists()
assert text_file.is_file()
assert json_file.exists()
assert json_file.is_file()
assert jsonl_file.exists()
assert jsonl_file.is_file()
# Test that the client can validate these files by calling create_benchmark_job
# We need to mock the update_token and the HTTP request to avoid actual API calls
with patch.object(self.client, 'update_token'):
with patch.object(self.client, '_generic_http_request_executor') as mock_executor:
# Mock the executor to return the job_id directly
mock_executor.return_value = "test_job_123"
# This should not raise InvalidFileException
result = self.client.create_benchmark_job(
text_file, json_file, jsonl_file, True
)
assert result == "test_job_123"
def test_client_state_management(self):
"""Test client state management."""
@@ -273,6 +269,7 @@ class TestCleverSwarmClient:
assert FileTypeAPI.AutoDetect == "AutoDetect"
assert FileTypeAPI.Markdown == "Markdown"
assert FileTypeAPI.PlainText == "PlainText"
assert FileTypeAPI.PDF == "PDF"
def test_response_type_enum_values(self):
"""Test ResponseTypeAPI enum values."""
@@ -850,4 +847,4 @@ class TestCleverSwarmClientHTTP:
raise ValueError("Unexpected error")
with pytest.raises(UnexpectedConditionException):
self.client._generic_http_request_executor(test_func)
self.client._generic_http_request_executor(test_func)
+7 -3
View File
@@ -448,13 +448,15 @@ class TestBenchmarkCLIUtilityMethods:
"type": "Benchmark",
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL"
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001"
}
]
mock_client.get_jobs_list.return_value = mock_jobs
mock_client_class.return_value = mock_client
self.cli._client = mock_client
self.cli.print_server_jobs()
captured = capsys.readouterr()
@@ -473,6 +475,8 @@ class TestBenchmarkCLIUtilityMethods:
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001",
"unstructured_sources": ["test.txt"],
"ontology_sources": ["test.json"],
"ground_truth_sources": ["gt.jsonl"]
@@ -481,7 +485,7 @@ class TestBenchmarkCLIUtilityMethods:
mock_client.get_jobs_list.return_value = mock_jobs
mock_client_class.return_value = mock_client
self.cli._client = mock_client
self.cli.print_server_jobs(detailed=True)
captured = capsys.readouterr()
@@ -60,6 +60,8 @@ class TestBenchmarkCLIExtended:
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001",
"unstructured_sources": ["test.txt"],
"ontology_sources": ["test.json"],
"ontology_spec_sources": ["test.owl"],
@@ -67,7 +69,7 @@ class TestBenchmarkCLIExtended:
"ground_truth_sources": ["test_gt.jsonl"]
}
]
with patch.object(self.cli, 'list_server_jobs') as mock_list:
mock_list.return_value = mock_jobs
self.cli.print_server_jobs(detailed=True)
@@ -82,13 +84,15 @@ class TestBenchmarkCLIExtended:
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001",
"unstructured_sources": ["test.txt"],
"ontology_sources": ["test.json"],
"ontology_spec_sources": ["test.owl"],
"ground_truth_sources": ["test_gt.jsonl"]
}
]
with patch.object(self.cli, 'list_server_jobs') as mock_list:
mock_list.return_value = mock_jobs
self.cli.print_server_jobs(detailed=True)
+10 -4
View File
@@ -534,11 +534,13 @@ class TestTextoToKGCLIJobPrinting:
"type": "UnstructuredWithOntology",
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL"
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001"
}
]
mock_list_jobs.return_value = mock_jobs
self.cli.print_server_jobs()
captured = capsys.readouterr()
@@ -556,13 +558,15 @@ class TestTextoToKGCLIJobPrinting:
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001",
"unstructured_sources": ["test.txt"],
"ontology_sources": ["test.json"],
"ontology_spec_sources": ["test.owl"]
}
]
mock_list_jobs.return_value = mock_jobs
self.cli.print_server_jobs(is_detailed=True)
captured = capsys.readouterr()
@@ -582,6 +586,8 @@ class TestTextoToKGCLIJobPrinting:
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001",
"unstructured_sources": ["test.txt"],
"ontology_sources": ["test.json"],
"ontology_spec_sources": ["test.owl"],
@@ -589,7 +595,7 @@ class TestTextoToKGCLIJobPrinting:
}
]
mock_list_jobs.return_value = mock_jobs
self.cli.print_server_jobs(is_detailed=True)
captured = capsys.readouterr()
@@ -182,13 +182,15 @@ class TestTextoToKGCLIExtended:
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001",
"unstructured_sources": ["test.txt"],
"ontology_sources": ["test.json"],
"ontology_spec_sources": ["test.owl"]
}
]
mock_client.get_jobs_list.return_value = mock_jobs
with patch('builtins.print') as mock_print:
self.cli.print_server_jobs()
assert mock_print.call_count > 0
@@ -204,6 +206,8 @@ class TestTextoToKGCLIExtended:
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001",
"unstructured_sources": ["test.txt"],
"ontology_sources": ["test.json"],
"ontology_spec_sources": ["test.owl"],
@@ -211,7 +215,7 @@ class TestTextoToKGCLIExtended:
}
]
mock_client.get_jobs_list.return_value = mock_jobs
with patch('builtins.print') as mock_print:
self.cli.print_server_jobs(is_detailed=True)
assert mock_print.call_count > 0
@@ -227,13 +231,15 @@ class TestTextoToKGCLIExtended:
"status": "Completed",
"retries_count": 0,
"file_type": "JSONL",
"machine_id": "machine-001",
"worker_id": "worker-001",
"unstructured_sources": ["test.txt"],
"ontology_sources": ["test.json"],
"ontology_spec_sources": ["test.owl"]
}
]
mock_client.get_jobs_list.return_value = mock_jobs
with patch('builtins.print') as mock_print:
self.cli.print_server_jobs(is_detailed=True)
assert mock_print.call_count > 0
+4 -1
View File
@@ -43,7 +43,10 @@ class TestDetailedEvalExtendedSimple:
encoder = CustomEncoder()
test_set = {1, 2, 3, "test"}
result = encoder.default(test_set)
assert result == [1, 2, 3, "test"]
# Convert result back to set for order-independent comparison
assert set(result) == test_set
# Also verify it's a list
assert isinstance(result, list)
def test_custom_encoder_with_non_set(self):
"""Test CustomEncoder with non-set objects."""
+20 -19
View File
@@ -30,6 +30,7 @@ class TestFileTypeAPI:
assert FileTypeAPI.AutoDetect == "AutoDetect"
assert FileTypeAPI.Markdown == "Markdown"
assert FileTypeAPI.PlainText == "PlainText"
assert FileTypeAPI.PDF == "PDF"
def test_enum_membership(self):
"""Test that FileTypeAPI members can be checked for membership."""
@@ -37,6 +38,7 @@ class TestFileTypeAPI:
assert FileTypeAPI.AutoDetect in FileTypeAPI
assert FileTypeAPI.Markdown in FileTypeAPI
assert FileTypeAPI.PlainText in FileTypeAPI
assert FileTypeAPI.PDF in FileTypeAPI
# Test string value membership (Python 3.12+ only)
import sys
@@ -44,13 +46,14 @@ class TestFileTypeAPI:
assert "AutoDetect" in FileTypeAPI
assert "Markdown" in FileTypeAPI
assert "PlainText" in FileTypeAPI
assert "PDF" in FileTypeAPI
assert "InvalidType" not in FileTypeAPI
def test_enum_iteration(self):
"""Test that FileTypeAPI can be iterated over."""
values = list(FileTypeAPI)
expected_values = ["AutoDetect", "Markdown", "PlainText"]
assert len(values) == 3
expected_values = ["AutoDetect", "Markdown", "PlainText", "PDF"]
assert len(values) == 4
for value in expected_values:
assert value in values
@@ -59,6 +62,7 @@ class TestFileTypeAPI:
assert FileTypeAPI.AutoDetect == "AutoDetect"
assert FileTypeAPI.Markdown == "Markdown"
assert FileTypeAPI.PlainText == "PlainText"
assert FileTypeAPI.PDF == "PDF"
assert FileTypeAPI.AutoDetect != FileTypeAPI.Markdown
def test_enum_string_representation(self):
@@ -66,6 +70,7 @@ class TestFileTypeAPI:
assert str(FileTypeAPI.AutoDetect) == "AutoDetect"
assert str(FileTypeAPI.Markdown) == "Markdown"
assert str(FileTypeAPI.PlainText) == "PlainText"
assert str(FileTypeAPI.PDF) == "PDF"
def test_enum_repr(self):
"""Test repr representation of FileTypeAPI values."""
@@ -73,18 +78,14 @@ class TestFileTypeAPI:
assert "FileTypeAPI.AutoDetect" in repr(FileTypeAPI.AutoDetect)
assert "FileTypeAPI.Markdown" in repr(FileTypeAPI.Markdown)
assert "FileTypeAPI.PlainText" in repr(FileTypeAPI.PlainText)
assert "FileTypeAPI.PDF" in repr(FileTypeAPI.PDF)
def test_enum_inheritance(self):
"""Test that FileTypeAPI inherits from StrEnum."""
from enum import StrEnum
# Import StrEnum from the same module where FileTypeAPI is defined
from cleverswarm_python_client.libs.file_type_enum import StrEnum
assert issubclass(FileTypeAPI, StrEnum)
def test_enum_equality_with_string(self):
"""Test that FileTypeAPI values are equal to their string representations."""
assert FileTypeAPI.AutoDetect == "AutoDetect"
assert FileTypeAPI.Markdown == "Markdown"
assert FileTypeAPI.PlainText == "PlainText"
def test_enum_case_sensitivity(self):
"""Test that FileTypeAPI values are case sensitive."""
assert FileTypeAPI.AutoDetect != "autodetect"
@@ -100,6 +101,7 @@ class TestFileType:
assert FileType.JSONL == "JSONL"
assert FileType.Markdown == "Markdown"
assert FileType.PlainText == "PlainText"
assert FileType.PDF == "PDF"
assert FileType.Unknown == "Unknown"
def test_enum_membership(self):
@@ -108,6 +110,7 @@ class TestFileType:
assert FileType.JSONL in FileType
assert FileType.Markdown in FileType
assert FileType.PlainText in FileType
assert FileType.PDF in FileType
assert FileType.Unknown in FileType
# Test string value membership (Python 3.12+ only)
@@ -116,14 +119,15 @@ class TestFileType:
assert "JSONL" in FileType
assert "Markdown" in FileType
assert "PlainText" in FileType
assert "PDF" in FileType
assert "Unknown" in FileType
assert "InvalidType" not in FileType
def test_enum_iteration(self):
"""Test that FileType can be iterated over."""
values = list(FileType)
expected_values = ["JSONL", "Markdown", "PlainText", "Unknown"]
assert len(values) == 4
expected_values = ["JSONL", "Markdown", "PlainText", "PDF", "Unknown"]
assert len(values) == 5
for value in expected_values:
assert value in values
@@ -132,6 +136,7 @@ class TestFileType:
assert FileType.JSONL == "JSONL"
assert FileType.Markdown == "Markdown"
assert FileType.PlainText == "PlainText"
assert FileType.PDF == "PDF"
assert FileType.Unknown == "Unknown"
assert FileType.JSONL != FileType.Markdown
@@ -140,6 +145,7 @@ class TestFileType:
assert str(FileType.JSONL) == "JSONL"
assert str(FileType.Markdown) == "Markdown"
assert str(FileType.PlainText) == "PlainText"
assert str(FileType.PDF) == "PDF"
assert str(FileType.Unknown) == "Unknown"
def test_enum_repr(self):
@@ -148,20 +154,15 @@ class TestFileType:
assert "FileType.JSONL" in repr(FileType.JSONL)
assert "FileType.Markdown" in repr(FileType.Markdown)
assert "FileType.PlainText" in repr(FileType.PlainText)
assert "FileType.PDF" in repr(FileType.PDF)
assert "FileType.Unknown" in repr(FileType.Unknown)
def test_enum_inheritance(self):
"""Test that FileType inherits from StrEnum."""
from enum import StrEnum
# Import StrEnum from the same module where FileType is defined
from cleverswarm_python_client.libs.file_type_enum import StrEnum
assert issubclass(FileType, StrEnum)
def test_enum_equality_with_string(self):
"""Test that FileType values are equal to their string representations."""
assert FileType.JSONL == "JSONL"
assert FileType.Markdown == "Markdown"
assert FileType.PlainText == "PlainText"
assert FileType.Unknown == "Unknown"
def test_enum_case_sensitivity(self):
"""Test that FileType values are case sensitive."""
assert FileType.JSONL != "jsonl"
+4 -2
View File
@@ -76,7 +76,8 @@ class TestJobType:
def test_enum_inheritance(self):
"""Test that JobType inherits from StrEnum."""
from enum import StrEnum
# Import StrEnum from the same module where JobType is defined
from cleverswarm_python_client.libs.job_enums import StrEnum
assert issubclass(JobType, StrEnum)
def test_enum_equality_with_string(self):
@@ -165,7 +166,8 @@ class TestJobStatus:
def test_enum_inheritance(self):
"""Test that JobStatus inherits from StrEnum."""
from enum import StrEnum
# Import StrEnum from the same module where JobStatus is defined
from cleverswarm_python_client.libs.job_enums import StrEnum
assert issubclass(JobStatus, StrEnum)
def test_enum_equality_with_string(self):
+2 -1
View File
@@ -70,7 +70,8 @@ class TestResponseTypeAPI:
def test_enum_inheritance(self):
"""Test that ResponseTypeAPI inherits from StrEnum."""
from enum import StrEnum
# Import StrEnum from the same module where ResponseTypeAPI is defined
from cleverswarm_python_client.libs.response_type_enum import StrEnum
assert issubclass(ResponseTypeAPI, StrEnum)
def test_enum_equality_with_string(self):
+2 -2
View File
@@ -20,7 +20,7 @@ deps =
isort>=5.9.1
commands =
flake8 --append-config=tox.ini src/cleverswarm_python_client
black -t py311 -t py312 -t py313 --check src/cleverswarm_python_client
black -t py310 -t py311 -t py312 -t py313 --check src/cleverswarm_python_client
isort --check-only --profile black src/cleverswarm_python_client
[testenv:type]
@@ -45,7 +45,7 @@ deps =
black>=25.9.0
isort>=5.9.1
commands =
black -t py311 -t py312 -t py313 src/cleverswarm_python_client tests
black -t py310 -t py311 -t py312 -t py313 src/cleverswarm_python_client tests
isort --profile black src/cleverswarm_python_client tests
[flake8]