the-datagarden 0.1.0__py3-none-any.whl → 1.2.3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ from .authentication import BaseDataGardenCredentials, DatagardenEnvironment
2
+
3
+ __all__ = ["DatagardenEnvironment", "BaseDataGardenCredentials"]
@@ -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,5 @@
1
+ from .authentication import AccessToken
2
+ from .authentication.environment import TheDatagardenProductionEnvironment
3
+ from .base import TheDataGardenAPI
4
+
5
+ __all__ = ["AccessToken", "TheDatagardenProductionEnvironment", "TheDataGardenAPI"]
@@ -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]
@@ -0,0 +1,4 @@
1
+ from .continent import Continent
2
+ from .country import Country
3
+
4
+ __all__ = ["Continent", "Country"]