Skip to content

Commit 4a7ff89

Browse files
authored
Enh/first code revamp (#20)
* Add multi-line parameter to isort configuration * Move response parsing to BaseService * Create and use ResponseFieldsMixin * Move response instances initialization to JSONResponse * Add relative imports instead of using library name * Small type hint enhance * Move response fields to separate modules for company enrichment and IP geolocation * Create and use _prepare_selected_fields to centralize selected fields preparation * Create and use NestedEntitiesMixin * Move all non-service code into core
1 parent bac58bf commit 4a7ff89

Some content is hidden

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

46 files changed

+354
-681
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ repos:
1717
rev: 5.11.5
1818
hooks:
1919
- id: isort
20+
args: [--multi-line=VERTICAL_HANGING_INDENT]
2021

2122
- repo: https://github.com/pycqa/pydocstyle
2223
rev: 6.3.0

src/abstract_api/avatars/avatars.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
from abstract_api.bases import BaseService
2-
from abstract_api.exceptions import ResponseParseError
3-
1+
from ..core.bases import BaseService
42
from .avatars_response import AvatarsResponse
53

64

7-
class Avatars(BaseService):
5+
class Avatars(BaseService[AvatarsResponse]):
86
"""AbstractAPI avatar generation service.
97
108
Used for creating highly customizable avatar images with a person's name or
@@ -66,7 +64,8 @@ def create(
6664
AvatarsResponse representing API call response.
6765
"""
6866
# TODO: Validation
69-
response = self._service_request(
67+
return self._service_request(
68+
_response_class=AvatarsResponse,
7069
name=name,
7170
image_size=image_size,
7271
image_format=image_format,
@@ -79,12 +78,3 @@ def create(
7978
is_italic=is_italic,
8079
is_bold=is_bold
8180
)
82-
83-
try:
84-
avatars_response = AvatarsResponse(response=response)
85-
except Exception as e:
86-
raise ResponseParseError(
87-
"Failed to parse response as AvatarsResponse"
88-
) from e
89-
90-
return avatars_response

src/abstract_api/avatars/avatars_response.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from abstract_api.bases import FileResponse
1+
from ..core.bases import FileResponse
22

33

44
class AvatarsResponse(FileResponse):
Lines changed: 13 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from typing import Iterable
22

3-
from abstract_api.bases import BaseService
4-
from abstract_api.exceptions import ClientRequestError, ResponseParseError
5-
3+
from ..core.bases import BaseService
4+
from ..core.mixins import ResponseFieldsMixin
65
from .company_enrichment_response import CompanyEnrichmentResponse
76
from .response_fields import RESPONSE_FIELDS
87

98

10-
class CompanyEnrichment(BaseService):
9+
class CompanyEnrichment(
10+
ResponseFieldsMixin,
11+
BaseService[CompanyEnrichmentResponse]
12+
):
1113
"""AbstractAPI company enrichment service.
1214
1315
Used to find a company's details using its domain.
@@ -16,65 +18,7 @@ class CompanyEnrichment(BaseService):
1618
_subdomain: Company enrichment service subdomain.
1719
"""
1820
_subdomain: str = "companyenrichment"
19-
20-
def __init__(
21-
self,
22-
*,
23-
response_fields: Iterable[str] | None = None,
24-
**kwargs
25-
) -> None:
26-
"""Constructs a CompanyEnrichment.
27-
28-
Args:
29-
response_fields: Selected response fields.
30-
"""
31-
super().__init__(**kwargs)
32-
if response_fields is not None:
33-
self.response_fields = frozenset(response_fields)
34-
else:
35-
self.response_fields = RESPONSE_FIELDS
36-
37-
@staticmethod
38-
def _validate_response_fields(response_fields: Iterable[str]) -> None:
39-
"""Validates whether all the given fields are acceptable.
40-
41-
Args:
42-
response_fields: Selected response fields.
43-
"""
44-
for field in response_fields:
45-
if field not in RESPONSE_FIELDS:
46-
raise ClientRequestError(
47-
f"Field '{field}' is not a valid response field for "
48-
f"Company Enrichment service."
49-
)
50-
51-
@property
52-
def response_fields(self) -> frozenset[str]:
53-
"""Gets selected response fields."""
54-
if self._response_fields:
55-
return self._response_fields
56-
return RESPONSE_FIELDS
57-
58-
@response_fields.setter
59-
def response_fields(self, fields: Iterable[str]) -> None:
60-
"""Sets selected response fields."""
61-
self._validate_response_fields(fields)
62-
self._response_fields = frozenset(fields)
63-
64-
@staticmethod
65-
def _response_fields_as_param(response_fields: Iterable[str]) -> str:
66-
"""Builds 'fields' URL query parameter.
67-
68-
Builds a string that contains selected response fields to be used
69-
as a URL query parameter.
70-
71-
Args:
72-
response_fields: Selected response fields.
73-
74-
Returns:
75-
Comma-separated string with all selected response fields.
76-
"""
77-
return ",".join(response_fields)
21+
_response_fields = RESPONSE_FIELDS
7822

7923
def check(
8024
self,
@@ -85,31 +29,16 @@ def check(
8529
8630
Args:
8731
domain: The domain of the company you want to get data from.
88-
fields: Selected response fields.
32+
fields: Selected response fields (optional).
8933
9034
Returns:
9135
CompanyEnrichmentResponse representing API call response.
9236
"""
93-
if fields:
94-
self._validate_response_fields(fields)
95-
response_fields = frozenset(fields)
96-
else:
97-
response_fields = self.response_fields
37+
selected_fields = self._prepare_selected_fields(fields)
9838

99-
# TODO: Handle request errors.
100-
response = self._service_request(
39+
return self._service_request(
40+
_response_class=CompanyEnrichmentResponse,
41+
_response_class_kwargs={"response_fields": selected_fields},
10142
domain=domain,
102-
fields=self._response_fields_as_param(response_fields)
43+
fields=self._response_fields_as_param(selected_fields)
10344
)
104-
105-
try:
106-
company_enrichment_response = CompanyEnrichmentResponse(
107-
response=response,
108-
response_fields=response_fields
109-
)
110-
except Exception as e:
111-
raise ResponseParseError(
112-
"Failed to parse response as CompanyEnrichmentResponse"
113-
) from e
114-
115-
return company_enrichment_response

src/abstract_api/company_enrichment/company_enrichment_response.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,9 @@
1-
from typing import TYPE_CHECKING
2-
3-
import requests.models
4-
5-
from abstract_api.bases import JSONResponse
1+
from ..core.bases import JSONResponse
62

73

84
class CompanyEnrichmentResponse(JSONResponse):
95
"""Company enrichment service response."""
106

11-
def __init__(
12-
self,
13-
response: requests.models.Response,
14-
response_fields: frozenset[str]
15-
) -> None:
16-
"""Initializes a new CompanyEnrichmentResponse."""
17-
super().__init__(response)
18-
self._response_fields = response_fields
19-
not_in_response = object()
20-
for field in response_fields:
21-
if TYPE_CHECKING:
22-
assert isinstance(self.meta.body_json, dict)
23-
value = self.meta.body_json.get(field, not_in_response)
24-
# Set property only if field was returned
25-
if value is not not_in_response:
26-
setattr(self, f"_{field}", value)
27-
287
@property
298
def name(self) -> str | None:
309
"""The name of the company."""

src/abstract_api/core/__init__.py

Whitespace-only changes.
File renamed without changes.

src/abstract_api/bases/base_service.py renamed to src/abstract_api/core/bases/base_service.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
from abc import ABC
21
from io import BytesIO
3-
from typing import Any, Final
2+
from typing import TYPE_CHECKING, Any, Final, Generic, Type, TypeVar
43

54
import requests
65
from requests import codes
76

8-
from abstract_api.exceptions import APIRequestError, ClientRequestError
7+
from ..exceptions import (
8+
APIRequestError,
9+
ClientRequestError,
10+
ResponseParseError
11+
)
912

13+
if TYPE_CHECKING:
14+
from ._base_response import BaseResponse
15+
BaseResponseT = TypeVar("BaseResponseT", bound="BaseResponse")
1016

11-
class BaseService(ABC):
17+
18+
class BaseService(Generic[BaseResponseT]):
1219
"""Base class for all AbstractAPI service classes.
1320
1421
Attributes:
@@ -43,12 +50,14 @@ def _service_url(self, action: str = "") -> str:
4350

4451
def _service_request(
4552
self,
53+
_response_class: Type[BaseResponseT],
54+
_response_class_kwargs: dict[str, Any] | None = None,
4655
_method: str = "GET",
4756
_body: dict[str, Any] | None = None,
4857
_files: dict[str, BytesIO] | None = None,
4958
_action: str = "",
5059
**params
51-
) -> requests.models.Response:
60+
) -> BaseResponseT:
5261
"""Makes the HTTP call to Abstract API service endpoint.
5362
5463
Args:
@@ -86,4 +95,17 @@ def _service_request(
8695

8796
if response.status_code not in [codes.OK, codes.NO_CONTENT]:
8897
APIRequestError.raise_from_response(response)
89-
return response
98+
99+
if _response_class_kwargs is None:
100+
_response_class_kwargs = {}
101+
102+
try:
103+
parsed_response = _response_class(
104+
response=response, **_response_class_kwargs
105+
)
106+
except Exception as e:
107+
raise ResponseParseError(
108+
f"Failed to parse response as {_response_class.__name__}"
109+
) from e
110+
111+
return parsed_response
File renamed without changes.

0 commit comments

Comments
 (0)