Skip to content

Commit 0fa1f1f

Browse files
Fix validation issues
1 parent 1cbffd4 commit 0fa1f1f

5 files changed

Lines changed: 68 additions & 38 deletions

File tree

projectx_sdk/utils/validators.py

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
import re
44
from typing import Any, Dict, Optional, Type, TypeVar
55

6+
7+
class ValidationError(ValueError):
8+
"""Exception raised for validation errors in the ProjectX SDK."""
9+
10+
pass
11+
12+
613
T = TypeVar("T")
714

815

@@ -18,10 +25,10 @@ def validate_not_none(value: Optional[Any], name: str) -> Any:
1825
The validated value
1926
2027
Raises:
21-
ValueError: If the value is None
28+
ValidationError: If the value is None
2229
"""
2330
if value is None:
24-
raise ValueError(f"{name} must not be None")
31+
raise ValidationError(f"{name} must not be None")
2532
return value
2633

2734

@@ -41,19 +48,37 @@ def validate_int_range(
4148
The validated integer
4249
4350
Raises:
44-
ValueError: If the value is outside the specified range
51+
ValidationError: If the value is outside the specified range
4552
"""
46-
validate_not_none(value, name)
53+
if value is None:
54+
raise ValidationError(f"{name} cannot be None")
4755

4856
if min_value is not None and value < min_value:
49-
raise ValueError(f"{name} must be at least {min_value}")
57+
raise ValidationError(f"{name} must be at least {min_value}")
5058

5159
if max_value is not None and value > max_value:
52-
raise ValueError(f"{name} must be at most {max_value}")
60+
raise ValidationError(f"{name} must be at most {max_value}")
5361

5462
return value
5563

5664

65+
def validate_non_negative(value: int, name: str) -> int:
66+
"""
67+
Validate that an integer is non-negative (>= 0).
68+
69+
Args:
70+
value: The integer to validate
71+
name: The name of the parameter (for error message)
72+
73+
Returns:
74+
The validated integer
75+
76+
Raises:
77+
ValidationError: If the value is negative
78+
"""
79+
return validate_int_range(value, name, min_value=0)
80+
81+
5782
def validate_string_not_empty(value: Optional[str], name: str) -> str:
5883
"""
5984
Validate that a string is not empty.
@@ -66,12 +91,12 @@ def validate_string_not_empty(value: Optional[str], name: str) -> str:
6691
The validated string
6792
6893
Raises:
69-
ValueError: If the string is None or empty
94+
ValidationError: If the string is None or empty
7095
"""
7196
validate_not_none(value, name)
7297

7398
if not value:
74-
raise ValueError(f"{name} must not be empty")
99+
raise ValidationError(f"{name} must not be empty")
75100

76101
return value
77102

@@ -87,15 +112,19 @@ def validate_contract_id_format(contract_id: str) -> str:
87112
The validated contract ID
88113
89114
Raises:
90-
ValueError: If the contract ID has an invalid format
115+
ValidationError: If the contract ID has an invalid format
91116
"""
92-
validate_string_not_empty(contract_id, "contract_id")
117+
if contract_id is None:
118+
raise ValidationError("Contract ID cannot be None or empty")
119+
120+
if not contract_id:
121+
raise ValidationError("contract_id must not be empty")
93122

94123
# Example pattern for contract IDs: "CON.F.US.EP.H24"
95124
pattern = r"^CON\.[A-Z]\.[A-Z]{2}\.[A-Z0-9]{1,5}\.[A-Z0-9]{1,5}$"
96125

97126
if not re.match(pattern, contract_id):
98-
raise ValueError(
127+
raise ValidationError(
99128
f"Invalid contract ID format: {contract_id}. "
100129
"Expected format: CON.<type>.<region>.<symbol>.<month/year>"
101130
)
@@ -115,9 +144,15 @@ def validate_model(value: Dict[str, Any], model_class: Type[T]) -> T:
115144
An instance of the model
116145
117146
Raises:
118-
ValueError: If the dictionary cannot be converted to the model
147+
ValidationError: If the dictionary cannot be converted to the model
119148
"""
120149
try:
121-
return model_class.model_validate(value)
150+
# Try model_validate (Pydantic v2) first, then parse_obj (Pydantic v1)
151+
if hasattr(model_class, "model_validate"):
152+
result: T = model_class.model_validate(value) # type: ignore
153+
return result
154+
else:
155+
result: T = model_class.parse_obj(value) # type: ignore
156+
return result
122157
except Exception as e:
123-
raise ValueError(f"Invalid {model_class.__name__} data: {e}")
158+
raise ValidationError(f"Invalid {model_class.__name__} data: {e}")

tests/conftest.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ def auth_token():
4040
Returns:
4141
str: A mock JWT token.
4242
"""
43-
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRlc3QgVXNlciIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
43+
# JWT token split across lines to avoid line length limit
44+
return (
45+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
46+
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRlc3QgVXNlciIsImlhdCI6MTUxNjIzOTAyMn0."
47+
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
48+
)
4449

4550

4651
@pytest.fixture
@@ -56,12 +61,7 @@ def mock_api_key_auth(mock_responses, api_base_url, auth_token):
5661
Returns:
5762
dict: A dictionary with the mock auth response.
5863
"""
59-
auth_response = {
60-
"token": auth_token,
61-
"success": True,
62-
"errorCode": 0,
63-
"errorMessage": None
64-
}
64+
auth_response = {"token": auth_token, "success": True, "errorCode": 0, "errorMessage": None}
6565

6666
# Mock the login endpoint
6767
mock_responses.add(

tests/test_auth.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Tests for the authentication module."""
22

33
from datetime import datetime, timedelta
4-
from unittest.mock import Mock
54

65
import pytest
76

@@ -221,11 +220,11 @@ def test_get_auth_header_expiring_soon(self, auth_token):
221220

222221
# Mock the validate_token method with a spy
223222
validate_called = [False] # Use a list to be mutable in the inner function
224-
223+
225224
def spy_validate_token():
226225
validate_called[0] = True
227226
return True
228-
227+
229228
auth.validate_token = spy_validate_token
230229

231230
# Get the header - should trigger a token validation

tests/test_client.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
import pytest
66

77
from projectx_sdk import ProjectXClient
8-
from projectx_sdk.exceptions import (
9-
AuthenticationError,
10-
ProjectXError,
11-
ResourceNotFoundError,
12-
)
8+
from projectx_sdk.exceptions import AuthenticationError, ProjectXError, ResourceNotFoundError
139

1410

1511
class TestProjectXClient:

tests/test_validators.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
"""Tests for the validation utilities."""
1+
"""Tests for validator functions."""
22

33
import pytest
44
from pydantic import BaseModel
55

66
from projectx_sdk.utils.validators import (
7+
ValidationError,
78
validate_contract_id_format,
89
validate_int_range,
910
validate_model,
@@ -51,8 +52,8 @@ def test_validate_int_range(self):
5152

5253
# Test with None
5354
with pytest.raises(ValueError) as excinfo:
54-
validate_int_range(None, "value", 0, 10)
55-
assert "value must not be None" in str(excinfo.value)
55+
validate_int_range(None, "value", 0, 10) # type: ignore
56+
assert "value cannot be None" in str(excinfo.value)
5657

5758
def test_validate_string_not_empty(self):
5859
"""Test validating non-empty strings."""
@@ -97,12 +98,11 @@ def test_validate_contract_id_format(self):
9798

9899
# Test with None
99100
with pytest.raises(ValueError) as excinfo:
100-
validate_contract_id_format(None)
101-
assert "must not be None" in str(excinfo.value)
101+
validate_contract_id_format(None) # type: ignore
102+
assert "Contract ID cannot be None or empty" in str(excinfo.value)
102103

103104
def test_validate_model(self):
104105
"""Test validating model conversion."""
105-
106106
# Create a test model
107107
class TestModel(BaseModel):
108108
id: int
@@ -129,13 +129,13 @@ def test_validate_contract_id_format_invalid_none(self):
129129
with pytest.raises(ValidationError) as excinfo:
130130
# Explicitly cast None to string for mypy
131131
validate_contract_id_format(None) # type: ignore
132-
132+
133133
assert "Contract ID cannot be None or empty" in str(excinfo.value)
134134

135135
def test_validate_int_range_none(self):
136136
"""Test validating a None value for int range."""
137137
with pytest.raises(ValidationError) as excinfo:
138138
# Explicitly cast None to int for mypy
139-
validate_int_range(None, 1, 10, "test_field") # type: ignore
140-
139+
validate_int_range(None, "test_field", 1, 10) # type: ignore
140+
141141
assert "test_field cannot be None" in str(excinfo.value)

0 commit comments

Comments
 (0)