the-datagarden 0.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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]
|