Skip to content

Commit 7e8317d

Browse files
authored
Feature/validations (#26)
* Add validation for avatars service parameters * Add validation for exchange rate service * Add validation for holidays service * Add validation for image processing service * Add validation for timezone service * Add validation for VAT service * Add validation for website screenshot service
1 parent 008f1a3 commit 7e8317d

File tree

9 files changed

+139
-7
lines changed

9 files changed

+139
-7
lines changed

src/abstract_api/avatars/avatars.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from ..core.bases import BaseService
2+
from ..core.exceptions import ClientRequestError
3+
from ..core.validators import numerical
24
from .avatars_response import AvatarsResponse
35

46

@@ -13,6 +15,23 @@ class Avatars(BaseService[AvatarsResponse]):
1315
"""
1416
_subdomain = "avatars"
1517

18+
@staticmethod
19+
def _validate_params(**kwargs) -> None:
20+
"""Validates passed service parameters."""
21+
ranged = {
22+
"image_size": (6, 512),
23+
"font_size": (0.1, 1.0),
24+
"char_limit": (1, 2)
25+
}
26+
for param, allowed_range in ranged.items():
27+
numerical.between(param, kwargs[param], *allowed_range)
28+
29+
image_format = kwargs["image_format"]
30+
if image_format and image_format not in ["png", "svg"]:
31+
raise ClientRequestError(
32+
"'image_format' must be either 'png' or 'svg'"
33+
)
34+
1635
def create(
1736
self,
1837
name: str,
@@ -63,7 +82,7 @@ def create(
6382
Returns:
6483
AvatarsResponse representing API call response.
6584
"""
66-
# TODO: Validation
85+
self._validate_params(**locals())
6786
return self._service_request(
6887
_response_class=AvatarsResponse,
6988
name=name,

src/abstract_api/core/validators/__init__.py

Whitespace-only changes.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import Any
2+
3+
from abstract_api.core.exceptions import ClientRequestError
4+
5+
6+
def between(
7+
param: str,
8+
value: Any,
9+
start: int | float,
10+
end: int | float
11+
) -> None:
12+
"""Validate a value to be in a range (inclusive).
13+
14+
Args:
15+
param: Parameter name.
16+
value: Parameter value.
17+
start: Beginning of the range.
18+
end: End of the range.
19+
20+
Raises:
21+
ClientRequestError if the given value is not in the range (inclusive).
22+
"""
23+
if value is None:
24+
return
25+
26+
if not (start <= value <= end):
27+
raise ClientRequestError(
28+
f"'{param}' must be in range from {start} to {end} (inclusive)"
29+
)
30+
31+
32+
def greater_or_equal(
33+
param: str,
34+
value: Any,
35+
threshold: int | float
36+
) -> None:
37+
"""Validate a value to be in a range (inclusive).
38+
39+
Args:
40+
param: Parameter name.
41+
value: Parameter value.
42+
threshold: Threshold that the given value must be greater than.
43+
44+
Raises:
45+
ClientRequestError if the given value is less than given threshold.
46+
"""
47+
if value is None:
48+
return
49+
50+
if value < threshold:
51+
raise ClientRequestError(
52+
f"'{param}' must be greater than or equal to {threshold}"
53+
)

src/abstract_api/exchange_rates/exchange_rates.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Iterable
22

33
from ..core.bases import BaseService
4+
from ..core.validators import numerical
45
from .exchange_rates_conversion_response import ExchangeRatesConversionResponse
56
from .historical_exchange_rates_response import HistoricalExchangeRatesResponse
67
from .live_exchange_rates_response import LiveExchangeRatesResponse
@@ -87,6 +88,7 @@ def convert(
8788
Returns:
8889
ExchangeRatesConversionResponse representing API call response.
8990
"""
91+
numerical.greater_or_equal("base_amount", base_amount, 0)
9092
return self._service_request(
9193
_response_class=ExchangeRatesConversionResponse,
9294
_action="convert",

src/abstract_api/holidays/holidays.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from ..core.bases import BaseService
2+
from ..core.validators import numerical
23
from .holidays_response import HolidaysResponse
34

45

@@ -13,6 +14,17 @@ class Holidays(BaseService[HolidaysResponse]):
1314
"""
1415
_subdomain = "holidays"
1516

17+
@staticmethod
18+
def _validate_params(**kwargs) -> None:
19+
"""Validates passed service parameters."""
20+
ranged = {
21+
"year": (1800, 2100),
22+
"month": (1, 12),
23+
"day": (1, 31)
24+
}
25+
for param, allowed_range in ranged.items():
26+
numerical.between(param, kwargs[param], *allowed_range)
27+
1628
def lookup(
1729
self,
1830
country: str,
@@ -41,6 +53,7 @@ def lookup(
4153
Returns:
4254
HolidaysResponse representing API call response.
4355
"""
56+
self._validate_params(**locals())
4457
return self._service_request(
4558
_response_class=HolidaysResponse,
4659
country=country,

src/abstract_api/image_processing/image_processing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from ..core.bases import BaseService
55
from ..core.exceptions import ClientRequestError
6+
from ..core.validators import numerical
67
from .image_processing_response import ImageProcessingResponse
78
from .strategies import BaseStrategy
89

@@ -107,6 +108,8 @@ def _process(
107108
if image is None and url is None:
108109
raise ClientRequestError("Image or URL must be passed")
109110

111+
numerical.between("quality", quality, 0, 100)
112+
110113
data: dict[str, Any] = {"api_key": self._api_key}
111114
if resize is not None:
112115
data["resize"] = resize.json()

src/abstract_api/timezone/timezone.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from typing import Annotated
2+
13
from ..core.bases import BaseService
4+
from ..core.exceptions import ClientRequestError
25
from .current_timezone_response import CurrentTimezoneResponse
36
from .timezone_conversion_response import TimezoneConversionResponse
47

@@ -13,9 +16,26 @@ class Timezone(BaseService):
1316
"""
1417
_subdomain = "timezone"
1518

19+
@staticmethod
20+
def _validate_location(
21+
param: str,
22+
location: str | Annotated[list[float], 2] | tuple[float, float]
23+
) -> None:
24+
"""Validates a given location.
25+
26+
Args:
27+
param: Name of the parameter passed to service.
28+
location: Value of location passed.
29+
"""
30+
if isinstance(location, (list, tuple)):
31+
if len(location) != 2:
32+
raise ClientRequestError(
33+
f"'{param}' must contain both/only longitude and latitude."
34+
)
35+
1636
@staticmethod
1737
def _location_as_param(
18-
location: str | list[float] | tuple[float, ...]
38+
location: str | Annotated[list[float], 2] | tuple[float, float]
1939
) -> str:
2040
"""Converts location to a request query parameter value.
2141
@@ -29,14 +49,13 @@ def _location_as_param(
2949
Returns:
3050
A string with location as query parameter value.
3151
"""
32-
if isinstance(location, list) or isinstance(location, tuple):
52+
if isinstance(location, (list, tuple)):
3353
location = ",".join(map(str, location))
34-
3554
return location
3655

3756
def current(
3857
self,
39-
location: str | list[float] | tuple[float, ...]
58+
location: str | Annotated[list[float], 2] | tuple[float, float]
4059
) -> CurrentTimezoneResponse:
4160
"""Finds the current time, date, and timezone of a given location.
4261
@@ -50,6 +69,7 @@ def current(
5069
Returns:
5170
CurrentTimezoneResponse representing API call response.
5271
"""
72+
self._validate_location("location", location)
5373
return self._service_request(
5474
_response_class=CurrentTimezoneResponse,
5575
_action="current_time",
@@ -58,8 +78,8 @@ def current(
5878

5979
def convert(
6080
self,
61-
base_location: str | list[float] | tuple[float, ...],
62-
target_location: str | list[float] | tuple[float, ...],
81+
base_location: str | Annotated[list[float], 2] | tuple[float, float],
82+
target_location: str | Annotated[list[float], 2] | tuple[float, float],
6383
base_datetime: str | None = None
6484
) -> TimezoneConversionResponse:
6585
"""Converts datetime of base location to target location's datetime.
@@ -83,6 +103,8 @@ def convert(
83103
Returns:
84104
TimezoneConversionResponse representing API call response.
85105
"""
106+
self._validate_location("base_location", base_location)
107+
self._validate_location("target_location", target_location)
86108
return self._service_request(
87109
_response_class=TimezoneConversionResponse,
88110
_action="convert_time",

src/abstract_api/vat/vat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from ..core.bases import BaseService
2+
from ..core.validators import numerical
23
from .vat_calculation_response import VATCalculationResponse
34
from .vat_categories_response import VATCategoriesResponse
45
from .vat_validation_response import VATValidationResponse
@@ -56,6 +57,7 @@ def calculate(
5657
Returns:
5758
VATCalculationResponse representing API call response.
5859
"""
60+
numerical.greater_or_equal("amount", amount, 0)
5961
return self._service_request(
6062
_response_class=VATCalculationResponse,
6163
_action="calculate",

src/abstract_api/website_screenshot/website_screenshot.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from ..core.bases import BaseService
2+
from ..core.exceptions import ClientRequestError
3+
from ..core.validators import numerical
24
from .website_screenshot_response import WebsiteScreenshotResponse
35

46

@@ -12,6 +14,21 @@ class WebsiteScreenshot(BaseService[WebsiteScreenshotResponse]):
1214
"""
1315
_subdomain = "screenshot"
1416

17+
@staticmethod
18+
def _validate_params(**kwargs) -> None:
19+
"""Validates passed service parameters."""
20+
capture_full_page = kwargs["capture_full_page"]
21+
dimensions = ["width", "height"]
22+
for d in dimensions:
23+
value = kwargs[d]
24+
if value is not None:
25+
if capture_full_page is not None and capture_full_page:
26+
raise ClientRequestError(
27+
f"'{d}' is not a valid option when"
28+
f" 'capture_full_page' is True"
29+
)
30+
numerical.greater_or_equal(d, value, 0)
31+
1532
def capture(
1633
self,
1734
url: str,
@@ -45,6 +62,7 @@ def capture(
4562
Returns:
4663
WebsiteScreenshotResponse representing API call response.
4764
"""
65+
self._validate_params(**locals())
4866
return self._service_request(
4967
_response_class=WebsiteScreenshotResponse,
5068
url=url,

0 commit comments

Comments
 (0)