Skip to content

Commit 57d80d7

Browse files
authored
Test/adding project tests (#31)
* Add initial Avatars tests * Add initial CompanyEnrichment tests * Create specific directories under tests directory * Add numerical validators tests * Add _base_response module tests * Add BaseService tests * Exclude 'if TYPE_CHECKING:' branches from test coverage * Add FileResponse tests * Set source directory for coverage * Add JSONResponseMeta tests * Add JSONResponse tests * Add Country tests * Add CompanyEnrichmentResponse tests * Add JSONResponse.__setattr__ test * Add APIRequestError tests * Add NestedEntitiesMixin tests * Add ResponseFieldsMixin tests * Add EmailValidation tests * Enhance TestCompanyEnrichment tests * Refactor _init_response_field in classes to use parent's logic * Add EmailValidationResponse test * Add MultipleExchangeRatesResponse test * Remove unused imports * Add HistoricalExchangeRatesResponse tests * Add ExchangeRatesConversionResponse tests * Add LiveExchangeRatesResponse tests * Add ExchangeRates tests * Add HolidaysResponse tests * Add Holidays tests * Add IBANValidationResponse test * Add IBANValidation test * Add CropModeMixin tests * Add HeightMixin tests * Add WidthMixin tests * Add Crop tests * Add BaseStrategy test * Add Fill tests * Add Square tests * Add IPGeolocationResponse test * Add IPGeolocation tests * Add an assertion to IPGeolocationResponse test * Add PhoneValidationResponse test * Add PhoneValidation test * Add CurrentTimezoneResponse test * Add TimezoneConversionResponse test * Add Timezone tests * Add VATValidationResponse test * Add VATCalculationResponse test * Add VATCategoriesResponse tests * Add VAT tests * Add WebScraping test * Fix Avatars test * Add WebsiteScreenshot tests
1 parent 1f40e00 commit 57d80d7

File tree

90 files changed

+2782
-29
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+2782
-29
lines changed

.coveragerc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[run]
2+
source = src
3+
4+
[report]
5+
exclude_lines =
6+
pragma: no cover
7+
if TYPE_CHECKING:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ __pycache__/
99

1010
# Distribution / packaging
1111
*.egg-info/
12+
13+
# Coverage.py
14+
.coverage

.pre-commit-config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
fail_fast: true
12
repos:
23
- repo: https://github.com/pre-commit/pre-commit-hooks
34
rev: v4.5.0
@@ -12,6 +13,7 @@ repos:
1213
hooks:
1314
- id: flake8
1415
args: [--max-line-length=79]
16+
exclude: "tests"
1517

1618
- repo: https://github.com/pycqa/isort
1719
rev: 5.11.5
@@ -26,12 +28,23 @@ repos:
2628
args:
2729
- --convention=google
2830
- --add-ignore=D100,D104 # Ignore missing docstring in public module or package
31+
exclude: "tests"
2932

3033
- repo: https://github.com/pre-commit/mirrors-mypy
3134
rev: v1.6.1
3235
hooks:
3336
- id: mypy
3437
additional_dependencies: ['types-requests']
38+
exclude: "tests"
39+
40+
- repo: local
41+
hooks:
42+
- id: unit-tests
43+
name: unit tests
44+
entry: pytest
45+
language: system
46+
pass_filenames: false
47+
always_run: true
3548

3649
- repo: https://github.com/Lucas-C/pre-commit-hooks
3750
rev: v1.5.4
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from typing import Final
22

33
from .avatars import Avatars
4+
from .avatars_response import AvatarsResponse
45

56
__all__: Final[list[str]] = [
6-
"Avatars"
7+
"Avatars",
8+
"AvatarsResponse"
79
]

src/abstract_api/core/bases/base_service.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ def _service_request(
102102
Parsed AbstractAPI's response.
103103
"""
104104
# Prepare HTTP method
105-
_method = _method.lower()
106-
if _method not in ["get", "post"]:
105+
_method = _method.upper()
106+
if _method not in ["GET", "POST"]:
107107
raise ClientRequestError(
108108
f"Invalid or not allowed HTTP method '{_method}'"
109109
)
@@ -113,7 +113,7 @@ def _service_request(
113113
"method": _method,
114114
"url": self.__service_url(_action)
115115
}
116-
if _method == "get":
116+
if _method == "GET":
117117
request_kwargs["params"] = {"api_key": self._api_key} | {
118118
# Ignore all None parameters, no need to transfer them over
119119
# the network call.
@@ -122,9 +122,9 @@ def _service_request(
122122
if value is not None
123123
}
124124
else:
125-
if _files:
125+
if _files is not None:
126126
request_kwargs["files"] = _files
127-
if _body:
127+
if _body is not None:
128128
request_kwargs["json"] = _body
129129

130130
# Make call

src/abstract_api/core/bases/json_response.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@ class JSONResponse(BaseResponse):
2828
_meta_class: ClassVar[Type[JSONResponseMeta]] = JSONResponseMeta
2929
meta: JSONResponseMeta
3030

31+
def __setattr__(self, key: str, value: Any) -> None:
32+
"""Sets a property only if it is not a response field.
33+
34+
The main reason for customizing the original method is that we use
35+
functools.cached_property, and it allows setting a property value after
36+
it is cached, even if it has no setter.
37+
"""
38+
response_fields_attr = "_response_fields"
39+
if key == response_fields_attr:
40+
if response_fields_attr in self.__dict__:
41+
raise AttributeError(
42+
f"'{response_fields_attr}' should not be changed"
43+
)
44+
elif key in self._response_fields:
45+
raise AttributeError(
46+
f"value of response field '{key}' should not be changed"
47+
)
48+
return super().__setattr__(key, value)
49+
3150
def _init_response_field(self, field: str, value: Any) -> None:
3251
"""Sets a response field's value during instance initialization.
3352
@@ -47,8 +66,9 @@ def __init__(
4766
list_response: bool = False
4867
) -> None:
4968
"""Initialize a new JSONResponse."""
69+
self._response_fields = response_fields # Must be set first.
70+
5071
super().__init__(response)
51-
self._response_fields = response_fields
5272

5373
if self.meta.body_json is None:
5474
return
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1-
from typing import Any, ClassVar, Type
1+
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, Type
22

33

4-
class NestedEntitiesMixin:
4+
class _ResponseFieldProtocol(Protocol):
5+
def _init_response_field(self, field: str, value: Any) -> None:
6+
...
7+
8+
9+
if TYPE_CHECKING:
10+
_Base = _ResponseFieldProtocol
11+
else:
12+
_Base = object
13+
14+
15+
class NestedEntitiesMixin(_Base):
516
"""Nested entities mixin for responses that have nested entities."""
617
_nested_entities: ClassVar[dict[str, Type]]
718

@@ -15,9 +26,6 @@ def _init_response_field(self, field: str, value: Any) -> None:
1526
value: Value to be set. The value is parsed to a nested entity
1627
if the field is a nested entity.
1728
"""
18-
setattr(
19-
self,
20-
f"_{field}",
21-
value if field not in self._nested_entities
22-
else self._nested_entities[field](**value)
23-
)
29+
if field in self._nested_entities:
30+
value = self._nested_entities[field](**value)
31+
super()._init_response_field(field, value)

src/abstract_api/email_validation/email_validation_response.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,9 @@ def _init_response_field(self, field: str, value: Any) -> None:
3030
value: Value to be set. The value is parsed to a nested entity
3131
if the field is a nested entity.
3232
"""
33-
setattr(
34-
self,
35-
f"_{field}",
36-
value if field not in self._complex_bool_fields
37-
else value["value"]
38-
)
33+
if field in self._complex_bool_fields:
34+
value = value["value"]
35+
super()._init_response_field(field, value)
3936

4037
def __init__(
4138
self,

src/abstract_api/exchange_rates/exchange_rates.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,14 @@ def convert(
8989
Returns:
9090
ExchangeRatesConversionResponse representing API call response.
9191
"""
92-
numerical.greater_or_equal("base_amount", base_amount, 0)
92+
if base_amount is not None:
93+
numerical.greater_or_equal("base_amount", base_amount, 0)
94+
9395
return self._service_request(
9496
_response_class=ExchangeRatesConversionResponse,
97+
_response_class_kwargs={
98+
"date_included_in_request": date is not None
99+
},
95100
_action="convert",
96101
base=base,
97102
target=target,

src/abstract_api/exchange_rates/exchange_rates_conversion_response.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,20 @@
99
class ExchangeRatesConversionResponse(JSONResponse):
1010
"""Exchange rate conversion service response."""
1111

12-
def __init__(self, response: requests.models.Response) -> None:
12+
def __init__(
13+
self,
14+
response: requests.models.Response,
15+
date_included_in_request: bool | None = False
16+
) -> None:
1317
"""Initializes a new ExchangeRateConversionResponse."""
14-
super().__init__(response, CONVERSION_RESPONSE_FIELDS)
18+
super().__init__(
19+
response,
20+
CONVERSION_RESPONSE_FIELDS - {
21+
"last_updated"
22+
if date_included_in_request
23+
else "date"
24+
}
25+
)
1526

1627
@cached_property
1728
def base(self) -> str | None:
@@ -27,7 +38,9 @@ def target(self) -> str | None:
2738
def date(self) -> str | None:
2839
"""The date the currencies were pulled from.
2940
30-
This is per successful request.
41+
This is per successful request and returned only when 'date' parameter
42+
is passed in request.
43+
This is mutually-exclusive with 'last_updated' in response.
3144
"""
3245
return self._get_response_field("date")
3346

@@ -52,5 +65,9 @@ def exchange_rate(self) -> float | None:
5265

5366
@cached_property
5467
def last_updated(self) -> int | None:
55-
"""The Unix timestamp of when the returned data was last updated."""
68+
"""The Unix timestamp of when the returned data was last updated.
69+
70+
This is returned if 'date' parameter was not passed in request.
71+
This is mutually-exclusive with 'date' in response.
72+
"""
5673
return self._get_response_field("last_updated")

0 commit comments

Comments
 (0)