the-datagarden 0.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- the_datagarden/__init__.py +8 -0
- the_datagarden/abc/__init__.py +3 -0
- the_datagarden/abc/api.py +19 -0
- the_datagarden/abc/authentication.py +42 -0
- the_datagarden/api/__init__.py +5 -0
- the_datagarden/api/authentication/__init__.py +112 -0
- the_datagarden/api/authentication/credentials/__init__.py +120 -0
- the_datagarden/api/authentication/environment/__init__.py +13 -0
- the_datagarden/api/authentication/settings.py +54 -0
- the_datagarden/api/base/__init__.py +215 -0
- the_datagarden/api/regions/__init__.py +4 -0
- the_datagarden/api/regions/base/__init__.py +108 -0
- the_datagarden/api/regions/base/settings.py +19 -0
- the_datagarden/api/regions/continent.py +9 -0
- the_datagarden/api/regions/country.py +9 -0
- the_datagarden/models/__init__.py +9 -0
- the_datagarden/models/geojson.py +179 -0
- the_datagarden/models/regional_data.py +411 -0
- the_datagarden/version.py +1 -0
- the_datagarden-1.2.1.dist-info/METADATA +137 -0
- the_datagarden-1.2.1.dist-info/RECORD +25 -0
- {the_datagarden-0.1.0.dist-info → the_datagarden-1.2.1.dist-info}/WHEEL +1 -1
- the_datagarden-0.1.0.dist-info/METADATA +0 -18
- the_datagarden-0.1.0.dist-info/RECORD +0 -7
- {the_datagarden-0.1.0.dist-info → the_datagarden-1.2.1.dist-info}/entry_points.txt +0 -0
- {the_datagarden-0.1.0.dist-info → the_datagarden-1.2.1.dist-info}/top_level.txt +0 -0
the_datagarden/__init__.py
CHANGED
@@ -0,0 +1,8 @@
|
|
1
|
+
from .api.authentication.environment import TheDatagardenLocalEnvironment
|
2
|
+
from .api.base import TheDataGardenAPI, TheDatagardenProductionEnvironment
|
3
|
+
|
4
|
+
__all__ = [
|
5
|
+
"TheDataGardenAPI",
|
6
|
+
"TheDatagardenProductionEnvironment",
|
7
|
+
"TheDatagardenLocalEnvironment",
|
8
|
+
]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from requests import Response
|
4
|
+
|
5
|
+
from the_datagarden.abc.authentication import DatagardenEnvironment
|
6
|
+
|
7
|
+
|
8
|
+
class BaseApi(ABC):
|
9
|
+
@abstractmethod
|
10
|
+
def __init__(self, environment: type[DatagardenEnvironment] | None = None): ...
|
11
|
+
|
12
|
+
@abstractmethod
|
13
|
+
def retrieve_from_api(
|
14
|
+
self,
|
15
|
+
url_extension: str,
|
16
|
+
method: str = "GET",
|
17
|
+
payload: dict | None = None,
|
18
|
+
params: dict | None = None,
|
19
|
+
) -> Response | None: ...
|
@@ -0,0 +1,42 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import TypedDict
|
3
|
+
|
4
|
+
|
5
|
+
class TheDatagardenCredentialsDict(TypedDict):
|
6
|
+
email: str
|
7
|
+
password: str
|
8
|
+
|
9
|
+
|
10
|
+
class BaseDataGardenCredentials(ABC):
|
11
|
+
"""Protocol for the datagarden credentials"""
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
@abstractmethod
|
15
|
+
def credentials(
|
16
|
+
cls, the_datagarden_api_url: str, email: str | None = None, password: str | None = None
|
17
|
+
) -> TheDatagardenCredentialsDict: ...
|
18
|
+
|
19
|
+
|
20
|
+
class DatagardenEnvironment(ABC):
|
21
|
+
"""Protocol for the datagarden environment"""
|
22
|
+
|
23
|
+
CREDENTIALS: type[BaseDataGardenCredentials]
|
24
|
+
THE_DATAGARDEN_URL: str
|
25
|
+
ECHO_INIT: bool = True
|
26
|
+
|
27
|
+
def __init__(self):
|
28
|
+
if self.ECHO_INIT:
|
29
|
+
print("Initializing :", self.__class__.__name__)
|
30
|
+
print("At :", self.the_datagarden_url)
|
31
|
+
|
32
|
+
def credentials(
|
33
|
+
self, email: str | None = None, password: str | None = None
|
34
|
+
) -> TheDatagardenCredentialsDict:
|
35
|
+
return self.CREDENTIALS.credentials(self.the_datagarden_url, email, password)
|
36
|
+
|
37
|
+
@property
|
38
|
+
def the_datagarden_url(self) -> str:
|
39
|
+
url = self.THE_DATAGARDEN_URL
|
40
|
+
if url[-1] != "/":
|
41
|
+
return url + "/"
|
42
|
+
return url
|
@@ -0,0 +1,112 @@
|
|
1
|
+
import json
|
2
|
+
from datetime import UTC, datetime, timedelta
|
3
|
+
|
4
|
+
import jwt
|
5
|
+
import requests
|
6
|
+
from requests import Response
|
7
|
+
|
8
|
+
from ...abc.authentication import DatagardenEnvironment
|
9
|
+
from .settings import (
|
10
|
+
BEARER_KEY,
|
11
|
+
DEFAULT_HEADER,
|
12
|
+
REFRESH_TOKEN_URL_EXTENSION,
|
13
|
+
REQ_TOKEN_URL_EXTENSION,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class AccessToken:
|
18
|
+
_instance = None
|
19
|
+
_tokens: dict = {}
|
20
|
+
TOKEN_LIFE_TIME_MARGIN: int = 20
|
21
|
+
ACCESS_TOKEN_KEY = "access"
|
22
|
+
REFRESH_TOKEN_KEY = "refresh"
|
23
|
+
|
24
|
+
def __new__(cls, *args, **kwargs):
|
25
|
+
if cls._instance is None:
|
26
|
+
cls._instance = super().__new__(cls)
|
27
|
+
return cls._instance
|
28
|
+
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
environment: type[DatagardenEnvironment],
|
32
|
+
email: str | None = None,
|
33
|
+
password: str | None = None,
|
34
|
+
) -> None:
|
35
|
+
self._environment = environment()
|
36
|
+
self._token_payload = self._environment.credentials(email, password)
|
37
|
+
self._token_header = DEFAULT_HEADER.copy()
|
38
|
+
|
39
|
+
@property
|
40
|
+
def _token_url(self) -> str:
|
41
|
+
return self._the_datagarden_url + REQ_TOKEN_URL_EXTENSION
|
42
|
+
|
43
|
+
@property
|
44
|
+
def _refresh_token_url(self) -> str:
|
45
|
+
return self._the_datagarden_url + REFRESH_TOKEN_URL_EXTENSION
|
46
|
+
|
47
|
+
@property
|
48
|
+
def _the_datagarden_url(self) -> str:
|
49
|
+
return self._environment.the_datagarden_url
|
50
|
+
|
51
|
+
def _access_token_expired(self) -> bool:
|
52
|
+
if self._token_expiry_time:
|
53
|
+
return datetime.now(tz=UTC) + timedelta(seconds=5) > self._token_expiry_time
|
54
|
+
return True
|
55
|
+
|
56
|
+
@property
|
57
|
+
def _access_token(self) -> str:
|
58
|
+
if not self._tokens:
|
59
|
+
self._request_tokens()
|
60
|
+
elif self._access_token_expired():
|
61
|
+
self._get_refresh_token()
|
62
|
+
return self._tokens.get(self.ACCESS_TOKEN_KEY, "")
|
63
|
+
|
64
|
+
@property
|
65
|
+
def header_with_access_token(self) -> dict[str, str]:
|
66
|
+
header = DEFAULT_HEADER.copy()
|
67
|
+
header["Authorization"] = BEARER_KEY + self._access_token
|
68
|
+
return header
|
69
|
+
|
70
|
+
def _request_tokens(self):
|
71
|
+
response = requests.request(
|
72
|
+
method="POST",
|
73
|
+
url=self._token_url,
|
74
|
+
headers=self._token_header,
|
75
|
+
data=json.dumps(self._token_payload),
|
76
|
+
)
|
77
|
+
if not response.status_code == 200:
|
78
|
+
print("Token request failed and returned error(s): ")
|
79
|
+
print(" Errorr : ", response.json().get("detail", "No error details provided"))
|
80
|
+
quit()
|
81
|
+
|
82
|
+
self._tokens = self._get_response_data(response)
|
83
|
+
self._set_token_expiry_time()
|
84
|
+
|
85
|
+
def _get_refresh_token(self):
|
86
|
+
response = requests.request(
|
87
|
+
method="POST",
|
88
|
+
url=self._refresh_token_url,
|
89
|
+
headers=self._token_header,
|
90
|
+
data=json.dumps(self._refresh_payload()),
|
91
|
+
)
|
92
|
+
if not response.status_code == 200:
|
93
|
+
raise ValueError("Token request failed and returned error: " f"{response.text}")
|
94
|
+
self._tokens = self._get_response_data(response)
|
95
|
+
self._set_token_expiry_time()
|
96
|
+
|
97
|
+
def _refresh_payload(self) -> dict:
|
98
|
+
refresh_token = self._tokens.get(self.REFRESH_TOKEN_KEY, "")
|
99
|
+
return {"refresh": refresh_token}
|
100
|
+
|
101
|
+
def _set_token_expiry_time(self):
|
102
|
+
if not self._tokens:
|
103
|
+
return
|
104
|
+
access_token = self._tokens.get(self.ACCESS_TOKEN_KEY, "")
|
105
|
+
if not access_token:
|
106
|
+
raise ValueError("Access token not found in response")
|
107
|
+
decoded_token = jwt.decode(access_token, options={"verify_signature": False})
|
108
|
+
exp_time_stamp = decoded_token["exp"]
|
109
|
+
self._token_expiry_time = datetime.fromtimestamp(timestamp=exp_time_stamp, tz=UTC)
|
110
|
+
|
111
|
+
def _get_response_data(self, response: Response) -> dict[str, str]:
|
112
|
+
return json.loads(response.text)
|
@@ -0,0 +1,120 @@
|
|
1
|
+
import requests
|
2
|
+
from decouple import config
|
3
|
+
|
4
|
+
from the_datagarden.abc.authentication import BaseDataGardenCredentials, TheDatagardenCredentialsDict
|
5
|
+
from the_datagarden.api.authentication.settings import REGISTRATION_URL_EXTENSION
|
6
|
+
|
7
|
+
|
8
|
+
class CredentialsFromUserInput:
|
9
|
+
def get_missing_credentials(self, the_datagarden_api_url: str) -> TheDatagardenCredentialsDict:
|
10
|
+
print()
|
11
|
+
print("Welcome to The Data Garden API.")
|
12
|
+
print()
|
13
|
+
print(" You can start using the API with an account from The-Datagarden.io.")
|
14
|
+
print(" Please provide your credentials or create a new account.")
|
15
|
+
print(" Check www.the-datagarden.io for more information.")
|
16
|
+
print()
|
17
|
+
choice = input(
|
18
|
+
"Do you want to (1) create a new account or (2) provide existing credentials? " "Enter 1 or 2: "
|
19
|
+
)
|
20
|
+
|
21
|
+
if choice == "1":
|
22
|
+
credentials = self.enroll_to_api(the_datagarden_api_url)
|
23
|
+
if not credentials:
|
24
|
+
quit()
|
25
|
+
return credentials
|
26
|
+
|
27
|
+
elif choice == "2":
|
28
|
+
return self.provide_existing_credentials()
|
29
|
+
else:
|
30
|
+
print("Invalid choice. Please enter 1 or 2.")
|
31
|
+
return self.get_missing_credentials(the_datagarden_api_url)
|
32
|
+
|
33
|
+
def enroll_to_api(self, the_datagarden_api_url: str) -> TheDatagardenCredentialsDict | None:
|
34
|
+
print("Enrolling in The Data Garden API...")
|
35
|
+
print()
|
36
|
+
email = input(" Enter your email: ")
|
37
|
+
password = self.get_confirmed_password()
|
38
|
+
print()
|
39
|
+
|
40
|
+
data = {"email": email, "password": password}
|
41
|
+
registration_url = the_datagarden_api_url + REGISTRATION_URL_EXTENSION
|
42
|
+
response = requests.post(registration_url, data=data)
|
43
|
+
if response.status_code == 201:
|
44
|
+
print("Successfully enrolled in The Data Garden API.")
|
45
|
+
return TheDatagardenCredentialsDict(
|
46
|
+
email=email,
|
47
|
+
password=password,
|
48
|
+
)
|
49
|
+
else:
|
50
|
+
print("Enrollment failed with error(s):")
|
51
|
+
for field, message_list in response.json().items():
|
52
|
+
print(f" {field}: {message_list[0]}")
|
53
|
+
|
54
|
+
return None
|
55
|
+
|
56
|
+
def get_confirmed_password(self) -> str:
|
57
|
+
while True:
|
58
|
+
password = input(" Enter your password: ")
|
59
|
+
confirm_password = input(" Confirm your password: ")
|
60
|
+
|
61
|
+
if password == confirm_password:
|
62
|
+
return password
|
63
|
+
else:
|
64
|
+
print("Passwords do not match. Please try again.")
|
65
|
+
|
66
|
+
def provide_existing_credentials(self) -> TheDatagardenCredentialsDict:
|
67
|
+
print("Please provide your existing credentials...")
|
68
|
+
print()
|
69
|
+
email = input(" Enter your email: ")
|
70
|
+
password = input(" Enter your password: ")
|
71
|
+
return TheDatagardenCredentialsDict(
|
72
|
+
email=email,
|
73
|
+
password=password,
|
74
|
+
)
|
75
|
+
|
76
|
+
def credentials(self, the_datagarden_api_url: str) -> TheDatagardenCredentialsDict:
|
77
|
+
return self.get_missing_credentials(the_datagarden_api_url)
|
78
|
+
|
79
|
+
|
80
|
+
class TheDataGardenCredentials(BaseDataGardenCredentials):
|
81
|
+
"""
|
82
|
+
Manages credentials for The Data Garden API in a production environment.
|
83
|
+
|
84
|
+
This class handles the retrieval of user credentials (email and password) for
|
85
|
+
authenticating with The Data Garden API. It first attempts to fetch credentials
|
86
|
+
from environment variables. If not found, it prompts the user for input.
|
87
|
+
|
88
|
+
Attributes:
|
89
|
+
ENV_EMAIL_KEY (str): Environment variable key for the user's email.
|
90
|
+
ENV_PASSWORD_KEY (str): Environment variable key for the user's password.
|
91
|
+
CREDENTIALS_FROM_USER_INPUT (CredentialsFromUserInput):
|
92
|
+
Instance to handle user input for credentials.
|
93
|
+
|
94
|
+
Methods:
|
95
|
+
credentials(): Retrieves and returns the user's credentials.
|
96
|
+
"""
|
97
|
+
|
98
|
+
ENV_EMAIL_KEY: str = "THE_DATAGARDEN_USER_EMAIL"
|
99
|
+
ENV_PASSWORD_KEY: str = "THE_DATAGARDEN_USER_PASSWORD"
|
100
|
+
CREDENTIALS_FROM_USER_INPUT = CredentialsFromUserInput()
|
101
|
+
|
102
|
+
@classmethod
|
103
|
+
def credentials(
|
104
|
+
cls, the_datagarden_api_url: str, email: str | None = None, password: str | None = None
|
105
|
+
) -> TheDatagardenCredentialsDict:
|
106
|
+
if email and password:
|
107
|
+
return TheDatagardenCredentialsDict(
|
108
|
+
email=email,
|
109
|
+
password=password,
|
110
|
+
)
|
111
|
+
|
112
|
+
datagarden_user_email = str(config(cls.ENV_EMAIL_KEY, default="", cast=str))
|
113
|
+
datagarden_user_password = str(config(cls.ENV_PASSWORD_KEY, default="", cast=str))
|
114
|
+
if datagarden_user_email and datagarden_user_password:
|
115
|
+
return TheDatagardenCredentialsDict(
|
116
|
+
email=datagarden_user_email,
|
117
|
+
password=datagarden_user_password,
|
118
|
+
)
|
119
|
+
|
120
|
+
return cls.CREDENTIALS_FROM_USER_INPUT.credentials(the_datagarden_api_url)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from the_datagarden.abc import DatagardenEnvironment
|
2
|
+
|
3
|
+
from ..credentials import TheDataGardenCredentials
|
4
|
+
|
5
|
+
|
6
|
+
class TheDatagardenProductionEnvironment(DatagardenEnvironment):
|
7
|
+
CREDENTIALS = TheDataGardenCredentials
|
8
|
+
THE_DATAGARDEN_URL = "https://api.the-datagarden.io"
|
9
|
+
|
10
|
+
|
11
|
+
class TheDatagardenLocalEnvironment(DatagardenEnvironment):
|
12
|
+
CREDENTIALS = TheDataGardenCredentials
|
13
|
+
THE_DATAGARDEN_URL = "http://127.0.0.1:8000"
|
@@ -0,0 +1,54 @@
|
|
1
|
+
"""
|
2
|
+
Authentication settings for The DataGarden API.
|
3
|
+
|
4
|
+
This module contains constants and default values used for authentication
|
5
|
+
and API communication with The DataGarden service.
|
6
|
+
|
7
|
+
Constants:
|
8
|
+
REQ_TOKEN_URL_EXTENSION (str): URL extension for requesting a token.
|
9
|
+
REFRESH_TOKEN_URL_EXTENSION (str): URL extension for refreshing a token.
|
10
|
+
HTTPS_PREFIX (str): Prefix for HTTPS URLs.
|
11
|
+
BEARER_KEY (str): Prefix for Bearer token authentication.
|
12
|
+
API_EXTENSION (str): Extension for API endpoints (currently empty).
|
13
|
+
DEFAULT_HEADER (dict): Default headers for API requests.
|
14
|
+
|
15
|
+
"""
|
16
|
+
|
17
|
+
from decouple import config
|
18
|
+
|
19
|
+
from the_datagarden.version import __version__
|
20
|
+
|
21
|
+
INCLUDE_STATISTIC_PARAM = {"include_statistics": "y"}
|
22
|
+
|
23
|
+
STATISTICS_URL_EXTENSION = "statistics/"
|
24
|
+
|
25
|
+
SHOW_REQ_DETAIL = config("SHOW_REQ_DETAIL", default=False, cast=bool)
|
26
|
+
REQ_TOKEN_URL_EXTENSION = "user/token/"
|
27
|
+
REFRESH_TOKEN_URL_EXTENSION = "user/token/refresh/"
|
28
|
+
|
29
|
+
REGISTRATION_URL_EXTENSION = "user/enroll/"
|
30
|
+
|
31
|
+
HTTPS_PREFIX = "https://"
|
32
|
+
BEARER_KEY = "Bearer "
|
33
|
+
API_EXTENSION = ""
|
34
|
+
|
35
|
+
DEFAULT_HEADER: dict[str, str] = {
|
36
|
+
"accept": "application/json",
|
37
|
+
"accept-language": "en,nl-NL;q=0.9,nl;q=0.8,sv;q=0.7",
|
38
|
+
"content-type": "application/json",
|
39
|
+
"X-The-Datagarden-Version": "The_datagarden_pypi_package_version__" + __version__,
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
class URLExtension:
|
44
|
+
WORLD = "world/"
|
45
|
+
WORLD_DATA = "world/regional_data/"
|
46
|
+
CONTINENTS = "continent/"
|
47
|
+
CONTINENT = "continent/"
|
48
|
+
COUNTRIES = "country/"
|
49
|
+
COUNTRY = "country/"
|
50
|
+
|
51
|
+
|
52
|
+
class DynamicEndpointCategories:
|
53
|
+
CONTINENTS = "continents"
|
54
|
+
COUNTRIES = "countries"
|
@@ -0,0 +1,215 @@
|
|
1
|
+
"""
|
2
|
+
Base module for The Data Garden API.
|
3
|
+
|
4
|
+
This module provides the foundation for interacting with The Data Garden API.
|
5
|
+
It includes the base class for API interactions and imports necessary
|
6
|
+
authentication components.
|
7
|
+
|
8
|
+
Classes:
|
9
|
+
TheDataGardenAPI: Base class for interacting with The Data Garden API.
|
10
|
+
|
11
|
+
Imports:
|
12
|
+
DatagardenEnvironment: Abstract base class for environment authentication.
|
13
|
+
TheDatagardenProductionEnvironment: Concrete implementation of the production
|
14
|
+
environment.
|
15
|
+
URLExtension: Class for handling URL extensions.
|
16
|
+
"""
|
17
|
+
|
18
|
+
from collections import defaultdict
|
19
|
+
from typing import Iterator
|
20
|
+
|
21
|
+
import requests
|
22
|
+
from requests import Response
|
23
|
+
|
24
|
+
from the_datagarden.abc.api import BaseApi
|
25
|
+
from the_datagarden.abc.authentication import DatagardenEnvironment
|
26
|
+
from the_datagarden.api.authentication import AccessToken
|
27
|
+
from the_datagarden.api.authentication.environment import TheDatagardenProductionEnvironment
|
28
|
+
from the_datagarden.api.authentication.settings import (
|
29
|
+
SHOW_REQ_DETAIL,
|
30
|
+
DynamicEndpointCategories,
|
31
|
+
URLExtension,
|
32
|
+
)
|
33
|
+
from the_datagarden.api.regions import Continent
|
34
|
+
from the_datagarden.api.regions.country import Country
|
35
|
+
|
36
|
+
|
37
|
+
class BaseDataGardenAPI(BaseApi):
|
38
|
+
"""
|
39
|
+
Base class for interacting with The Data Garden API.
|
40
|
+
"""
|
41
|
+
|
42
|
+
ACCESS_TOKEN: type[AccessToken] = AccessToken
|
43
|
+
DYNAMIC_ENDPOINTS: dict = defaultdict(dict)
|
44
|
+
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
environment: type[DatagardenEnvironment] | None = None,
|
48
|
+
email: str | None = None,
|
49
|
+
password: str | None = None,
|
50
|
+
):
|
51
|
+
self._environment = environment or TheDatagardenProductionEnvironment
|
52
|
+
self._tokens = self.ACCESS_TOKEN(self._environment, email, password)
|
53
|
+
self._base_url = self._environment().the_datagarden_url
|
54
|
+
|
55
|
+
def _generate_url(self, url_extension: str) -> str:
|
56
|
+
url = self._base_url + url_extension
|
57
|
+
if url[-1] != "/":
|
58
|
+
url += "/"
|
59
|
+
return url
|
60
|
+
|
61
|
+
def retrieve_from_api(
|
62
|
+
self,
|
63
|
+
url_extension: str,
|
64
|
+
method: str = "GET",
|
65
|
+
payload: dict | None = None,
|
66
|
+
params: dict | None = None,
|
67
|
+
) -> Response | None:
|
68
|
+
url = self._generate_url(url_extension)
|
69
|
+
headers = self._tokens.header_with_access_token
|
70
|
+
if SHOW_REQ_DETAIL:
|
71
|
+
print(f"Request URL: {url}")
|
72
|
+
print(f"Request headers: {headers}")
|
73
|
+
print(f"Request method: {method}")
|
74
|
+
print(f"Request payload: {payload}")
|
75
|
+
print(f"Request params: {params}")
|
76
|
+
match method:
|
77
|
+
case "GET":
|
78
|
+
response = requests.get(url, params=params, headers=headers)
|
79
|
+
case "POST":
|
80
|
+
response = requests.post(url, json=payload, headers=headers)
|
81
|
+
case _:
|
82
|
+
raise ValueError(f"Invalid method: {method}")
|
83
|
+
|
84
|
+
return self._response_handler(response)
|
85
|
+
|
86
|
+
def _response_handler(self, response: requests.Response) -> Response | None:
|
87
|
+
if response.status_code == 200:
|
88
|
+
return response
|
89
|
+
else:
|
90
|
+
response_dict = response.json()
|
91
|
+
for k, v in response_dict.items():
|
92
|
+
print(k)
|
93
|
+
print(v)
|
94
|
+
return None
|
95
|
+
|
96
|
+
def _get_next_page(self, response: requests.Response) -> requests.Response | None:
|
97
|
+
next_url = response.json().get("next")
|
98
|
+
if not next_url:
|
99
|
+
return None
|
100
|
+
|
101
|
+
# Determine the original request method
|
102
|
+
original_method = response.request.method
|
103
|
+
|
104
|
+
headers = self._tokens.header_with_access_token
|
105
|
+
|
106
|
+
if original_method == "GET":
|
107
|
+
return requests.get(next_url, headers=headers)
|
108
|
+
elif original_method == "POST":
|
109
|
+
# For POST requests, we need to preserve the original payload
|
110
|
+
original_payload = response.request.body
|
111
|
+
return requests.post(next_url, data=original_payload, headers=headers)
|
112
|
+
else:
|
113
|
+
raise ValueError(f"Unsupported method for pagination: {original_method}")
|
114
|
+
|
115
|
+
def _records_from_paginated_api_response(self, response: requests.Response | None) -> Iterator[dict]:
|
116
|
+
while response:
|
117
|
+
for record in response.json()["results"]:
|
118
|
+
yield record
|
119
|
+
response = self._get_next_page(response)
|
120
|
+
|
121
|
+
def _create_url_extension(self, url_extensions: list[str]) -> str:
|
122
|
+
url = "/".join(url_extensions).lower().replace(" ", "-")
|
123
|
+
if url_extensions[-1] == "/":
|
124
|
+
return url
|
125
|
+
return url + "/"
|
126
|
+
|
127
|
+
|
128
|
+
class TheDataGardenAPI(BaseDataGardenAPI):
|
129
|
+
_instance = None
|
130
|
+
_initialized = False
|
131
|
+
|
132
|
+
def __new__(cls, *args, **kwargs):
|
133
|
+
if cls._instance is None:
|
134
|
+
cls._instance = super().__new__(cls)
|
135
|
+
return cls._instance
|
136
|
+
|
137
|
+
def __init__(
|
138
|
+
self,
|
139
|
+
environment: type[DatagardenEnvironment] | None = None,
|
140
|
+
email: str | None = None,
|
141
|
+
password: str | None = None,
|
142
|
+
):
|
143
|
+
if not self._initialized:
|
144
|
+
super().__init__(environment, email, password)
|
145
|
+
self._setup_continents()
|
146
|
+
self._setup_countries()
|
147
|
+
self.__class__._initialized = True
|
148
|
+
|
149
|
+
def __getattr__(self, attr: str):
|
150
|
+
for _, endpoints in self.DYNAMIC_ENDPOINTS.items():
|
151
|
+
if attr.lower() in endpoints:
|
152
|
+
return endpoints[attr.lower()]
|
153
|
+
|
154
|
+
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")
|
155
|
+
|
156
|
+
def world(self):
|
157
|
+
response = self.retrieve_from_api(URLExtension.WORLD)
|
158
|
+
return response.json()
|
159
|
+
|
160
|
+
def continents(self, include_details: bool = False) -> list[str] | dict:
|
161
|
+
continents = (
|
162
|
+
self.DYNAMIC_ENDPOINTS.get(DynamicEndpointCategories.CONTINENTS, None) or self._setup_continents()
|
163
|
+
)
|
164
|
+
if not include_details:
|
165
|
+
return continents.keys()
|
166
|
+
return continents
|
167
|
+
|
168
|
+
def countries(self, include_details: bool = False) -> list[str] | dict:
|
169
|
+
countries = (
|
170
|
+
self.DYNAMIC_ENDPOINTS.get(DynamicEndpointCategories.COUNTRIES, None) or self._setup_countries()
|
171
|
+
)
|
172
|
+
if not include_details:
|
173
|
+
return countries.keys()
|
174
|
+
return countries
|
175
|
+
|
176
|
+
def _setup_continents(self):
|
177
|
+
if not self.DYNAMIC_ENDPOINTS.get(DynamicEndpointCategories.CONTINENTS, None):
|
178
|
+
continents = self.retrieve_from_api(URLExtension.CONTINENTS)
|
179
|
+
for continent in self._records_from_paginated_api_response(continents):
|
180
|
+
continent_method_name = continent["name"].lower().replace(" ", "_")
|
181
|
+
self.DYNAMIC_ENDPOINTS[DynamicEndpointCategories.CONTINENTS].update(
|
182
|
+
{
|
183
|
+
continent_method_name: Continent(
|
184
|
+
url=self._create_url_extension([URLExtension.CONTINENT + continent["name"]]),
|
185
|
+
api=self,
|
186
|
+
name=continent["name"].lower(),
|
187
|
+
),
|
188
|
+
}
|
189
|
+
)
|
190
|
+
|
191
|
+
return self.DYNAMIC_ENDPOINTS[DynamicEndpointCategories.CONTINENTS]
|
192
|
+
|
193
|
+
def _setup_countries(self):
|
194
|
+
if not self.DYNAMIC_ENDPOINTS.get(DynamicEndpointCategories.COUNTRIES, None):
|
195
|
+
countries = self.retrieve_from_api(URLExtension.COUNTRIES)
|
196
|
+
if not countries:
|
197
|
+
return None
|
198
|
+
for country in self._records_from_paginated_api_response(countries):
|
199
|
+
country_method_name = country["name"].lower().replace(" ", "_")
|
200
|
+
country_code = country["iso_cc_2"].lower()
|
201
|
+
continent = country["parent_region"].lower()
|
202
|
+
country = Country(
|
203
|
+
url=self._create_url_extension([URLExtension.COUNTRY + country["name"]]),
|
204
|
+
api=self,
|
205
|
+
name=country["name"],
|
206
|
+
continent=continent,
|
207
|
+
)
|
208
|
+
self.DYNAMIC_ENDPOINTS[DynamicEndpointCategories.COUNTRIES].update(
|
209
|
+
{
|
210
|
+
country_method_name: country,
|
211
|
+
country_code: country,
|
212
|
+
}
|
213
|
+
)
|
214
|
+
|
215
|
+
return self.DYNAMIC_ENDPOINTS[DynamicEndpointCategories.COUNTRIES]
|