sentor-ml 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sentor/__init__.py +3 -0
- sentor/client.py +86 -0
- sentor/exceptions.py +21 -0
- sentor_ml-1.0.0.dist-info/METADATA +122 -0
- sentor_ml-1.0.0.dist-info/RECORD +10 -0
- sentor_ml-1.0.0.dist-info/WHEEL +5 -0
- sentor_ml-1.0.0.dist-info/entry_points.txt +2 -0
- sentor_ml-1.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_client.py +132 -0
sentor/__init__.py
ADDED
sentor/client.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .exceptions import SentorAPIError, RateLimitError, AuthenticationError
|
|
3
|
+
from typing import TypedDict, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DocumentInput(TypedDict):
|
|
7
|
+
"""Represents a document to be analyzed with its metadata."""
|
|
8
|
+
|
|
9
|
+
doc_id: str
|
|
10
|
+
doc: str
|
|
11
|
+
entities: List[str]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SentorClient:
|
|
15
|
+
"""Client for interacting with the Sentor ML API."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
api_key: str,
|
|
20
|
+
base_url: str = "https://ml.sentor.app/api",
|
|
21
|
+
timeout: int = 30,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize the Sentor client with API credentials and configuration.
|
|
25
|
+
"""
|
|
26
|
+
self.api_key = api_key
|
|
27
|
+
self.base_url = base_url
|
|
28
|
+
self.timeout = timeout
|
|
29
|
+
self.headers = {
|
|
30
|
+
"x-api-key": self.api_key,
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def analyze(self, documents: List[DocumentInput]):
|
|
35
|
+
"""Analyze documents for sentiment and entity extraction.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
documents: List of documents to analyze
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
dict: Analysis results
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If input is empty
|
|
45
|
+
RateLimitError: If API rate limit is exceeded
|
|
46
|
+
AuthenticationError: If API key is invalid
|
|
47
|
+
SentorAPIError: For other API errors
|
|
48
|
+
"""
|
|
49
|
+
if not documents:
|
|
50
|
+
raise ValueError("Input is required")
|
|
51
|
+
|
|
52
|
+
url = f"{self.base_url}/ml/predict"
|
|
53
|
+
payload = {"docs": documents}
|
|
54
|
+
response = requests.post(
|
|
55
|
+
url, json=payload, headers=self.headers, timeout=self.timeout
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if response.status_code == 200:
|
|
59
|
+
return response.json()
|
|
60
|
+
elif response.status_code == 429:
|
|
61
|
+
raise RateLimitError(response.json())
|
|
62
|
+
elif response.status_code == 401:
|
|
63
|
+
raise AuthenticationError(response.json())
|
|
64
|
+
else:
|
|
65
|
+
raise SentorAPIError(response.json())
|
|
66
|
+
|
|
67
|
+
def check_health(self):
|
|
68
|
+
"""Check the health status of the Sentor API.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
dict: Health status information
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
SentorAPIError: If health check fails
|
|
75
|
+
"""
|
|
76
|
+
url = f"{self.base_url}/health"
|
|
77
|
+
response = requests.get(
|
|
78
|
+
url,
|
|
79
|
+
headers=self.headers,
|
|
80
|
+
timeout=self.timeout,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if response.status_code == 200:
|
|
84
|
+
return response.json()
|
|
85
|
+
else:
|
|
86
|
+
raise SentorAPIError(response.json())
|
sentor/exceptions.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class SentorAPIError(Exception):
|
|
2
|
+
"""Base exception for all Sentor API errors."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, response):
|
|
5
|
+
self.message = response.get("detail", "An error occurred")
|
|
6
|
+
self.code = response.get("status_code", "unknown")
|
|
7
|
+
super().__init__(self.message)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RateLimitError(SentorAPIError):
|
|
11
|
+
"""Exception raised when API rate limit is exceeded."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, response):
|
|
14
|
+
super().__init__(response)
|
|
15
|
+
self.retry_after = response.get("retry_after", 60)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthenticationError(SentorAPIError):
|
|
19
|
+
"""Exception raised when API authentication fails."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sentor-ml
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python SDK for interacting with the Sentor ML API for sentiment analysis
|
|
5
|
+
Home-page: https://github.com/NIKX-Tech/sentor-ml-python-sdk
|
|
6
|
+
Author: NIKX Technologies
|
|
7
|
+
Author-email: sentor@nikx.one
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Requires-Python: >=3.7
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: requests>=2.28.0
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: author-email
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# Sentor Python SDK
|
|
32
|
+
|
|
33
|
+
A Python SDK for interacting with the Sentor ML API for sentiment analysis. This SDK provides a simple and intuitive interface for sentiment analysis operations.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install sentor-ml
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- 🚀 Python 3.7+ support
|
|
44
|
+
- ⚡ Simple and intuitive API
|
|
45
|
+
- 🌍 Support for multiple languages
|
|
46
|
+
- 📦 Batch processing capabilities
|
|
47
|
+
- 🛡️ Comprehensive error handling
|
|
48
|
+
- 🔄 Real-time sentiment analysis
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Basic Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from sentor import SentorClient
|
|
56
|
+
|
|
57
|
+
# Initialize the client
|
|
58
|
+
client = SentorClient('your-api-key')
|
|
59
|
+
|
|
60
|
+
# Analyze sentiment
|
|
61
|
+
input_data = [
|
|
62
|
+
{
|
|
63
|
+
"doc": "Apple's new iPhone is amazing!",
|
|
64
|
+
"doc_id": "1",
|
|
65
|
+
"entities": [
|
|
66
|
+
"Apple",
|
|
67
|
+
"iPhone"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"doc": "Samsung's new phone is amazing!",
|
|
72
|
+
"doc_id": "2",
|
|
73
|
+
"entities": [
|
|
74
|
+
"Samsung",
|
|
75
|
+
"phone"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
result = client.analyze(input_data)
|
|
80
|
+
print(result)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Sample Output
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"results": [
|
|
88
|
+
{
|
|
89
|
+
"doc_id": "1",
|
|
90
|
+
"predicted_class": 2,
|
|
91
|
+
"predicted_label": "positive",
|
|
92
|
+
"probabilities": {
|
|
93
|
+
"negative": 0.00010637386003509164,
|
|
94
|
+
"neutral": 0.0002509312762413174,
|
|
95
|
+
"positive": 0.9996427297592163
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"doc_id": "2",
|
|
100
|
+
"predicted_class": 2,
|
|
101
|
+
"predicted_label": "positive",
|
|
102
|
+
"probabilities": {
|
|
103
|
+
"negative": 0.00010637386003509164,
|
|
104
|
+
"neutral": 0.0002509312762413174,
|
|
105
|
+
"positive": 0.9996427297592163
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## API Reference
|
|
113
|
+
|
|
114
|
+
Please refer to the [Sentor ML API Documentation](https://ml.sentor.app) for more details.
|
|
115
|
+
|
|
116
|
+
## Contributing
|
|
117
|
+
|
|
118
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sentor/__init__.py,sha256=K0z9SVT_Bkif2bj3Yqr7QtvQNTJll6an3DCuE0-rD0c,64
|
|
2
|
+
sentor/client.py,sha256=6Sps6UFLmUaEiE69yY8C537OQTtLteGLROs3N6om5Ss,2519
|
|
3
|
+
sentor/exceptions.py,sha256=MY5uFUJd_WBhCMc0zNxVvoasC1rME4Brqv82MwhDM7c,648
|
|
4
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
tests/test_client.py,sha256=kuhFtgwehMGqC0JKJfzAvqEl6UTvYo2nZDNa7YRF4rs,4101
|
|
6
|
+
sentor_ml-1.0.0.dist-info/METADATA,sha256=EhHpLSuh4y8VYjHGhPrrU4zGBU9VnbR8HhJd-ri0iRY,2993
|
|
7
|
+
sentor_ml-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
sentor_ml-1.0.0.dist-info/entry_points.txt,sha256=iYoIGXM8AusRG36JhqnBREngOd0iDOYN0C8PpiHtgXE,46
|
|
9
|
+
sentor_ml-1.0.0.dist-info/top_level.txt,sha256=nVfiI7y35XyGm5wyA94o24nDInXLiheycaaRtkR10ko,13
|
|
10
|
+
sentor_ml-1.0.0.dist-info/RECORD,,
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_client.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from sentor import SentorClient
|
|
3
|
+
from sentor.exceptions import (
|
|
4
|
+
SentorAPIError,
|
|
5
|
+
RateLimitError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
)
|
|
8
|
+
from unittest.mock import Mock, patch
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_client():
|
|
13
|
+
"""Create a mock SentorClient instance for testing."""
|
|
14
|
+
client = SentorClient("test-key")
|
|
15
|
+
return client
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_client_initialization():
|
|
19
|
+
"""Test proper initialization of SentorClient with default parameters."""
|
|
20
|
+
client = SentorClient(api_key="test-key")
|
|
21
|
+
assert isinstance(client, SentorClient)
|
|
22
|
+
assert client.api_key == "test-key"
|
|
23
|
+
assert client.base_url == "https://ml.sentor.app/api"
|
|
24
|
+
assert client.timeout == 30
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@patch("requests.post")
|
|
28
|
+
def test_analyze_success(mock_post):
|
|
29
|
+
"""Test successful document analysis with positive sentiment."""
|
|
30
|
+
# Setup mock response
|
|
31
|
+
mock_response = Mock()
|
|
32
|
+
mock_response.status_code = 200
|
|
33
|
+
mock_response.json.return_value = {
|
|
34
|
+
"results": [
|
|
35
|
+
{
|
|
36
|
+
"predicted_label": "positive",
|
|
37
|
+
"probabilities": {"positive": 0.95},
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
mock_post.return_value = mock_response
|
|
42
|
+
|
|
43
|
+
# Test the analyze method
|
|
44
|
+
client = SentorClient("test-key")
|
|
45
|
+
documents = [{"doc_id": "1", "doc": "Test text", "entities": []}]
|
|
46
|
+
result = client.analyze(documents)
|
|
47
|
+
|
|
48
|
+
assert result["results"][0]["predicted_label"] == "positive"
|
|
49
|
+
assert result["results"][0]["probabilities"]["positive"] == 0.95
|
|
50
|
+
mock_post.assert_called_once()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@patch("requests.post")
|
|
54
|
+
def test_analyze_rate_limit(mock_post):
|
|
55
|
+
"""Test rate limit error handling during document analysis."""
|
|
56
|
+
# Setup mock response for rate limit error
|
|
57
|
+
mock_response = Mock()
|
|
58
|
+
mock_response.status_code = 429
|
|
59
|
+
mock_response.json.return_value = {
|
|
60
|
+
"detail": "Rate limit exceeded",
|
|
61
|
+
"retry_after": 60,
|
|
62
|
+
}
|
|
63
|
+
mock_post.return_value = mock_response
|
|
64
|
+
|
|
65
|
+
# Test rate limit error
|
|
66
|
+
client = SentorClient("test-key")
|
|
67
|
+
documents = [{"doc_id": "1", "doc": "Test text", "entities": []}]
|
|
68
|
+
|
|
69
|
+
with pytest.raises(RateLimitError) as exc_info:
|
|
70
|
+
client.analyze(documents)
|
|
71
|
+
|
|
72
|
+
assert exc_info.value.retry_after == 60
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@patch("requests.post")
|
|
76
|
+
def test_analyze_auth_error(mock_post):
|
|
77
|
+
"""Test authentication error handling during document analysis."""
|
|
78
|
+
# Setup mock response for authentication error
|
|
79
|
+
mock_response = Mock()
|
|
80
|
+
mock_response.status_code = 401
|
|
81
|
+
mock_response.json.return_value = {"detail": "Invalid API key"}
|
|
82
|
+
mock_post.return_value = mock_response
|
|
83
|
+
|
|
84
|
+
# Test authentication error
|
|
85
|
+
client = SentorClient("test-key")
|
|
86
|
+
documents = [{"doc_id": "1", "doc": "Test text", "entities": []}]
|
|
87
|
+
|
|
88
|
+
with pytest.raises(AuthenticationError):
|
|
89
|
+
client.analyze(documents)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_analyze_empty_input():
|
|
93
|
+
"""Test validation of empty input in document analysis."""
|
|
94
|
+
client = SentorClient("test-key")
|
|
95
|
+
with pytest.raises(ValueError, match="Input is required"):
|
|
96
|
+
client.analyze([])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@patch("requests.get")
|
|
100
|
+
def test_check_health_success(mock_get):
|
|
101
|
+
"""Test successful health check response."""
|
|
102
|
+
# Setup mock response
|
|
103
|
+
mock_response = Mock()
|
|
104
|
+
mock_response.status_code = 200
|
|
105
|
+
mock_response.json.return_value = {"status": "healthy"}
|
|
106
|
+
mock_get.return_value = mock_response
|
|
107
|
+
|
|
108
|
+
# Test health check
|
|
109
|
+
client = SentorClient("test-key")
|
|
110
|
+
result = client.check_health()
|
|
111
|
+
|
|
112
|
+
assert result["status"] == "healthy"
|
|
113
|
+
mock_get.assert_called_once()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@patch("requests.get")
|
|
117
|
+
def test_check_health_error(mock_get):
|
|
118
|
+
"""Test error handling during health check."""
|
|
119
|
+
# Setup mock response for error
|
|
120
|
+
mock_response = Mock()
|
|
121
|
+
mock_response.status_code = 500
|
|
122
|
+
mock_response.json.return_value = {"detail": "Internal server error"}
|
|
123
|
+
mock_get.return_value = mock_response
|
|
124
|
+
|
|
125
|
+
# Test health check error
|
|
126
|
+
client = SentorClient("test-key")
|
|
127
|
+
with pytest.raises(SentorAPIError):
|
|
128
|
+
client.check_health()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
pytest.main([__file__])
|