Skip to content

Commit 900e97c

Browse files
authored
Feature/timezone (#16)
* Add current timezone service * Add timezone conversion service
1 parent 424d826 commit 900e97c

File tree

8 files changed

+365
-0
lines changed

8 files changed

+365
-0
lines changed

src/abstract_api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .iban_validation import IBANValidation
99
from .ip_geolocation import IPGeolocation
1010
from .phone_validation import PhoneValidation
11+
from .timezone import Timezone
1112
from .vat import VAT
1213

1314
__all__: Final[list[str]] = [
@@ -19,5 +20,6 @@
1920
"IBANValidation",
2021
"IPGeolocation",
2122
"PhoneValidation",
23+
"Timezone",
2224
"VAT"
2325
]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from typing import Final
2+
3+
from .current_timezone_response import CurrentTimezoneResponse
4+
from .timezone import Timezone
5+
from .timezone_conversion_response import TimezoneConversionResponse
6+
7+
__all__: Final[list[str]] = [
8+
"CurrentTimezoneResponse",
9+
"Timezone",
10+
"TimezoneConversionResponse"
11+
]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from typing import TYPE_CHECKING
2+
3+
import requests
4+
5+
from abstract_api.bases import JSONResponse
6+
7+
from .response_fields import CURRENT_RESPONSE_FIELDS
8+
9+
10+
class CurrentTimezoneResponse(JSONResponse):
11+
"""Current timezone service response."""
12+
13+
def __init__(self, response: requests.models.Response) -> None:
14+
"""Initializes a new CurrentTimezoneResponse."""
15+
super().__init__(response)
16+
self._response_fields = CURRENT_RESPONSE_FIELDS
17+
not_in_response = object()
18+
for field in CURRENT_RESPONSE_FIELDS:
19+
if TYPE_CHECKING:
20+
assert isinstance(self.meta.body_json, dict)
21+
value = self.meta.body_json.get(field, not_in_response)
22+
# Set property only if field was returned
23+
if value is not not_in_response:
24+
# TODO: Move to parent class
25+
setattr(self, f"_{field}", value)
26+
27+
@property
28+
def datetime(self) -> str | None:
29+
"""The current date and time of the requested_location."""
30+
return self._get_response_field("datetime")
31+
32+
@property
33+
def timezone_name(self) -> str | None:
34+
"""Timezone’s name from IANA Time Zone Database.
35+
36+
Read more: https://www.iana.org/time-zones
37+
"""
38+
return self._get_response_field("timezone_name")
39+
40+
@property
41+
def timezone_location(self) -> str | None:
42+
"""Timezone’s location."""
43+
return self._get_response_field("timezone_location")
44+
45+
@property
46+
def timezone_abbreviation(self) -> str | None:
47+
"""Timezone’s abbreviation, also from IANA Time Zone Database."""
48+
return self._get_response_field("timezone_abbreviation")
49+
50+
@property
51+
def gmt_offset(self) -> str | None:
52+
"""Timezone’s offset from Greenwich Mean Time (GMT).
53+
54+
Read more: https://greenwichmeantime.com/what-is-gmt
55+
"""
56+
return self._get_response_field("gmt_offset")
57+
58+
@property
59+
def is_dst(self) -> str | None:
60+
"""Whether the location is currently in Daylight Savings Time (DST).
61+
62+
Read more: https://wikipedia.org/wiki/Daylight_saving_time
63+
"""
64+
return self._get_response_field("is_dst")
65+
66+
@property
67+
def requested_location(self) -> str | None:
68+
"""The location from the request."""
69+
return self._get_response_field("requested_location")
70+
71+
@property
72+
def latitude(self) -> float | None:
73+
"""Decimal of the longitude found for the requested_location."""
74+
return self._get_response_field("latitude")
75+
76+
@property
77+
def longitude(self) -> float | None:
78+
"""Decimal of the longitude found for the requested_location."""
79+
return self._get_response_field("longitude")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from typing import Final
2+
3+
from .conversion import CONVERSION_RESPONSE_FIELDS
4+
from .current import CURRENT_RESPONSE_FIELDS
5+
6+
__all__: Final[list[str]] = [
7+
"CONVERSION_RESPONSE_FIELDS",
8+
"CURRENT_RESPONSE_FIELDS"
9+
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Response fields of timezone conversion service endpoint."""
2+
3+
4+
CONVERSION_RESPONSE_FIELDS: frozenset[str] = frozenset({
5+
"base_location",
6+
"target_location"
7+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Response fields of current timezone service endpoint."""
2+
3+
4+
CURRENT_RESPONSE_FIELDS: frozenset[str] = frozenset({
5+
"datetime",
6+
"timezone_name",
7+
"timezone_location",
8+
"timezone_abbreviation",
9+
"gmt_offset",
10+
"is_dst",
11+
"requested_location",
12+
"latitude",
13+
"longitude"
14+
})
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from abstract_api.bases import BaseService
2+
from abstract_api.exceptions import ResponseParseError
3+
4+
from .current_timezone_response import CurrentTimezoneResponse
5+
from .timezone_conversion_response import TimezoneConversionResponse
6+
7+
8+
class Timezone(BaseService):
9+
"""AbstractAPI timezone service.
10+
11+
Used to find, convert, and manage time and timezone data across the world.
12+
13+
Attributes:
14+
_subdomain: timezone service subdomain.
15+
"""
16+
_subdomain: str = "timezone"
17+
18+
@staticmethod
19+
def _location_as_param(
20+
location: str | list[float] | tuple[float, ...]
21+
) -> str:
22+
"""Converts location to a request query parameter value.
23+
24+
This method converts the location from list/tuple of floats to a
25+
string if the location given is a list/tuple.
26+
It does nothing if the given location is already a string.
27+
28+
Args:
29+
location: Location to be converted.
30+
31+
Returns:
32+
A string with location as query parameter value.
33+
"""
34+
if isinstance(location, list) or isinstance(location, tuple):
35+
location = ",".join(map(str, location))
36+
37+
return location
38+
39+
def current(
40+
self,
41+
location: str | list[float] | tuple[float, ...]
42+
) -> CurrentTimezoneResponse:
43+
"""Finds the current time, date, and timezone of a given location.
44+
45+
Args:
46+
location: The location to determine the current time and
47+
timezone of. This parameter accepts the location as
48+
a string (e.g., Los Angeles, CA),
49+
a longitude and latitude (e.g., -31.4173391,-64.183319),
50+
or an IP address (e.g., 82.111.111.111).
51+
52+
Returns:
53+
CurrentTimezoneResponse representing API call response.
54+
"""
55+
response = self._service_request(
56+
action="current_time",
57+
location=self._location_as_param(location)
58+
)
59+
60+
try:
61+
current_timezone_response = CurrentTimezoneResponse(
62+
response=response
63+
)
64+
except Exception as e:
65+
raise ResponseParseError(
66+
"Failed to parse response as CurrentTimezoneResponse"
67+
) from e
68+
69+
return current_timezone_response
70+
71+
def convert(
72+
self,
73+
base_location: str | list[float] | tuple[float, ...],
74+
target_location: str | list[float] | tuple[float, ...],
75+
base_datetime: str | None = None
76+
) -> TimezoneConversionResponse:
77+
"""Converts datetime of base location to target location's datetime.
78+
79+
By default, it converts the current time, but the conversion can
80+
take place in either the past or future with a simple parameter.
81+
82+
Args:
83+
base_location: The location you use as a base to convert the
84+
datetime for. This parameter accepts the location as
85+
a string (e.g., Los Angeles, CA),
86+
a longitude and latitude (e.g., -31.4173391,-64.183319),
87+
or an IP address (e.g., 82.111.111.111).
88+
target_location: The location you want to get the datetime for.
89+
This parameter accepts the location as
90+
a string (e.g., Los Angeles, CA),
91+
a longitude and latitude (e.g., -31.4173391,-64.183319),
92+
or an IP address (e.g., 82.111.111.111).
93+
base_datetime: The datetime you’re converting.
94+
95+
Returns:
96+
TimezoneConversionResponse representing API call response.
97+
"""
98+
response = self._service_request(
99+
action="convert_time",
100+
base_location=self._location_as_param(base_location),
101+
target_location=self._location_as_param(target_location),
102+
base_datetime=base_datetime
103+
)
104+
105+
try:
106+
timezone_conversion_response = TimezoneConversionResponse(
107+
response=response
108+
)
109+
except Exception as e:
110+
raise ResponseParseError(
111+
"Failed to parse response as TimezoneConversionResponse"
112+
) from e
113+
114+
return timezone_conversion_response
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from typing import TYPE_CHECKING, Final, Type
2+
3+
import requests
4+
5+
from abstract_api.bases import JSONResponse
6+
7+
from .response_fields import CONVERSION_RESPONSE_FIELDS
8+
9+
10+
class Timezone:
11+
"""Timezone entity in timezone conversion response.
12+
13+
Used to represent base and target location timezones.
14+
"""
15+
16+
def __init__(
17+
self,
18+
datetime: str,
19+
timezone_name: str,
20+
timezone_location: str,
21+
timezone_abbreviation: str,
22+
gmt_offset: int,
23+
is_dst: bool,
24+
requested_location: str,
25+
latitude: float,
26+
longitude: float
27+
) -> None:
28+
"""Initializes a new Timezone."""
29+
self._datetime = datetime
30+
self._timezone_name = timezone_name
31+
self._timezone_location = timezone_location
32+
self._timezone_abbreviation = timezone_abbreviation
33+
self._gmt_offset = gmt_offset
34+
self._is_dst = is_dst
35+
self._requested_location = requested_location
36+
self._latitude = latitude
37+
self._longitude = longitude
38+
39+
@property
40+
def datetime(self) -> str | None:
41+
"""The current date and time."""
42+
return self._datetime
43+
44+
@property
45+
def timezone_name(self) -> str | None:
46+
"""Timezone’s name from IANA Time Zone Database.
47+
48+
Read more: https://www.iana.org/time-zones
49+
"""
50+
return self._timezone_name
51+
52+
@property
53+
def timezone_location(self) -> str | None:
54+
"""Timezone’s location."""
55+
return self._timezone_location
56+
57+
@property
58+
def timezone_abbreviation(self) -> str | None:
59+
"""Timezone’s abbreviation, also from IANA Time Zone Database."""
60+
return self._timezone_abbreviation
61+
62+
@property
63+
def gmt_offset(self) -> int | None:
64+
"""Timezone’s offset from Greenwich Mean Time (GMT).
65+
66+
Read more: https://greenwichmeantime.com/what-is-gmt
67+
"""
68+
return self._gmt_offset
69+
70+
@property
71+
def is_dst(self) -> bool | None:
72+
"""Whether the location is currently in Daylight Savings Time (DST).
73+
74+
Read more: https://wikipedia.org/wiki/Daylight_saving_time
75+
"""
76+
return self._is_dst
77+
78+
@property
79+
def requested_location(self) -> str | None:
80+
"""The location from the request."""
81+
return self._requested_location
82+
83+
@property
84+
def latitude(self) -> float | None:
85+
"""Decimal of the longitude found for the requested_location."""
86+
return self._latitude
87+
88+
@property
89+
def longitude(self) -> float | None:
90+
"""Decimal of the longitude found for the requested_location."""
91+
return self._longitude
92+
93+
94+
class TimezoneConversionResponse(JSONResponse):
95+
"""Timezone conversion service response."""
96+
97+
_nested_entities: Final[dict[str, Type[Timezone]]] = {
98+
"base_location": Timezone,
99+
"target_location": Timezone
100+
}
101+
102+
def __init__(self, response: requests.models.Response) -> None:
103+
"""Initializes a new TimezoneConversionResponse."""
104+
super().__init__(response)
105+
self._response_fields = CONVERSION_RESPONSE_FIELDS
106+
not_in_response = object()
107+
for field in CONVERSION_RESPONSE_FIELDS:
108+
if TYPE_CHECKING:
109+
assert isinstance(self.meta.body_json, dict)
110+
value = self.meta.body_json.get(field, not_in_response)
111+
# Set property only if field was returned
112+
if value is not not_in_response:
113+
# TODO: Move to parent class
114+
setattr(
115+
self,
116+
f"_{field}",
117+
value if field not in self._nested_entities
118+
else self._nested_entities[field](**value)
119+
)
120+
121+
@property
122+
def base_location(self) -> Timezone:
123+
"""The time and timezone details of base location from request."""
124+
return self._get_response_field("base_location")
125+
126+
@property
127+
def target_location(self) -> Timezone:
128+
"""The time and timezone details of target location from request."""
129+
return self._get_response_field("target_location")

0 commit comments

Comments
 (0)