aerapi 0.1.1__tar.gz

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.
aerapi-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.1
2
+ Name: aerapi
3
+ Version: 0.1.1
4
+ Summary: A package for accessing APIs used by the data team
5
+ Home-page: https://github.com/stelltec/aerapi.git
6
+ Author: S. Nicholson
7
+ Author-email: Sean.Nicholson@aerlytix.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.6
13
+ Requires-Dist: requests
14
+ Requires-Dist: PyYAML
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.1
2
+ Name: aerapi
3
+ Version: 0.1.1
4
+ Summary: A package for accessing APIs used by the data team
5
+ Home-page: https://github.com/stelltec/aerapi.git
6
+ Author: S. Nicholson
7
+ Author-email: Sean.Nicholson@aerlytix.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.6
13
+ Requires-Dist: requests
14
+ Requires-Dist: PyYAML
@@ -0,0 +1,24 @@
1
+ setup.py
2
+ aerAPI.egg-info/PKG-INFO
3
+ aerAPI.egg-info/SOURCES.txt
4
+ aerAPI.egg-info/dependency_links.txt
5
+ aerAPI.egg-info/requires.txt
6
+ aerAPI.egg-info/top_level.txt
7
+ aerapi.egg-info/PKG-INFO
8
+ aerapi.egg-info/SOURCES.txt
9
+ aerapi.egg-info/dependency_links.txt
10
+ aerapi.egg-info/requires.txt
11
+ aerapi.egg-info/top_level.txt
12
+ common/__init__.py
13
+ common/auth.py
14
+ common/config_utils.py
15
+ common/utils.py
16
+ config/__init__.py
17
+ external/__init__.py
18
+ external/api_client.py
19
+ external/external_utils.py
20
+ internal/__init__.py
21
+ internal/api_client.py
22
+ internal/internal_utils.py
23
+ tests/test_external.py
24
+ tests/test_internal.py
@@ -0,0 +1,2 @@
1
+ requests
2
+ PyYAML
@@ -0,0 +1,4 @@
1
+ common
2
+ config
3
+ external
4
+ internal
@@ -0,0 +1,4 @@
1
+ # Optional: Import commonly used utilities
2
+ from .auth import get_auth_headers, check_authentication_status
3
+ from .config_utils import get_api_config
4
+ from .utils import chunkIt
@@ -0,0 +1,110 @@
1
+ import requests
2
+ import logging
3
+ import common.utils as utils
4
+
5
+ logging.basicConfig(level=logging.INFO)
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def get_auth_headers(api_key_id, api_secret_key):
10
+ """
11
+ Create the headers needed for authentication with API keys.
12
+
13
+ Parameters:
14
+ - api_key_id (str): The API key ID from the config file
15
+ - api_secret_key (str): The API secret key from the config file
16
+
17
+ Returns:
18
+ - dict: Headers containing the API key ID and secret key
19
+ """
20
+ headers = {
21
+ 'Api-Key-Id': api_key_id,
22
+ 'Api-Secret-Key': api_secret_key,
23
+ 'Content-Type': 'application/json'
24
+ }
25
+ return headers
26
+
27
+
28
+ def check_authentication_status(response):
29
+ """
30
+ Check if the API response indicates authentication failure.
31
+
32
+ Parameters:
33
+ - response (requests.Response): The response from an API request
34
+
35
+ Returns:
36
+ - bool: True if authentication failed, False otherwise
37
+ """
38
+ if response.status_code == 401:
39
+ # Authentication failed
40
+ print("Authentication failed. Please check your API keys.")
41
+ return False
42
+ return True
43
+
44
+
45
+ def make_authenticated_request(config, url, method='GET', params=None, data=None, debug=False, multiSend=False, sendSize=100, timeout=30):
46
+ """
47
+ Make an authenticated request to a given API, with optional support for sending requests in chunks.
48
+ """
49
+ base_url = config['base_url']
50
+ api_key_id = config['api_key_id']
51
+ api_secret_key = config['api_secret_key']
52
+
53
+ # print(base_url)
54
+ # print(api_key_id)
55
+ # print(api_secret_key)
56
+
57
+ full_url = f"{base_url.rstrip('/')}{url}"
58
+ if debug:
59
+ logger.info(f"Request URL: {full_url}")
60
+
61
+ headers = get_auth_headers(api_key_id, api_secret_key)
62
+ responses = []
63
+
64
+ # Multi-send logic
65
+ if multiSend and data and isinstance(data, dict) and 'items' in data:
66
+ items = data['items']
67
+ chunks = utils.chunkIt(items, max(1, len(items) // sendSize))
68
+
69
+ for chunk in chunks:
70
+ chunked_data = {"items": chunk}
71
+ try:
72
+ response = _send_request(method, full_url, headers, chunked_data, params, timeout, debug=debug)
73
+ if response:
74
+ responses.append(response)
75
+ except requests.exceptions.RequestException as e:
76
+ logger.error(f"Error during chunked request: {e}")
77
+ return None
78
+
79
+ return responses
80
+
81
+ # Single request logic
82
+ try:
83
+ return _send_request(method, full_url, headers, data, params, timeout, debug=debug)
84
+ except requests.exceptions.RequestException as e:
85
+ logger.error(f"Error during request: {e}")
86
+ return None
87
+
88
+
89
+ def _send_request(method, url, headers, data, params, timeout, debug):
90
+ if method == 'GET':
91
+ response = requests.get(url, headers=headers, params=params, timeout=timeout)
92
+ elif method == 'POST':
93
+ response = requests.post(url, headers=headers, json=data, timeout=timeout)
94
+ elif method == 'PUT':
95
+ response = requests.put(url, headers=headers, json=data, timeout=timeout)
96
+ elif method == 'DELETE':
97
+ response = requests.delete(url, headers=headers, params=params, timeout=timeout)
98
+ else:
99
+ raise ValueError(f"Unsupported HTTP method: {method}")
100
+
101
+ # Check if authentication or other errors occurred
102
+ if not check_authentication_status(response):
103
+ return None
104
+
105
+ # Log the status code for each request
106
+ if debug:
107
+ logger.info(f"Response Status Code: {response.status_code}")
108
+
109
+ # Return JSON response if content exists, otherwise return an empty dict
110
+ return response.json() if response.content else {}
@@ -0,0 +1,238 @@
1
+ import yaml
2
+ import os
3
+ import re
4
+
5
+
6
+
7
+ def get_api_config(env, name):
8
+ """
9
+ Get the API configuration for a specific environment and API name.
10
+
11
+ Parameters:
12
+ - env (str): The environment (e.g., 'preprod', 'demo').
13
+ - name (str): The specific name of the API within the environment (e.g., 'api-api-preprod-amergin').
14
+
15
+ Returns:
16
+ - dict: A dictionary containing 'api_key_id', 'api_secret_key', and 'base_url'.
17
+ """
18
+ try:
19
+ # Open and load the YAML configuration file
20
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
+ yaml_path = os.path.join(base_dir, 'config', 'config.yaml')
22
+
23
+ # Open and load the YAML configuration file
24
+ with open(yaml_path, 'r') as file:
25
+ config = yaml.safe_load(file)
26
+
27
+ # Loop through the configurations for the given environment
28
+ for entry in config['environments'].get(env, []):
29
+ if entry['name'] == name:
30
+ # Return the API key, secret, and name as base_url
31
+ return {
32
+ 'api_key_id': entry['api_key_id'],
33
+ 'api_secret_key': entry['api_secret_key'],
34
+ 'base_url': 'https://' + entry['url'] + '/v1'
35
+ }
36
+
37
+ # If no matching configuration is found, return None
38
+ print(f"No configuration found for {env} environment with API name {name}.")
39
+ return None
40
+
41
+ except FileNotFoundError:
42
+ print("Error: config.yaml file not found.")
43
+ return None
44
+ except yaml.YAMLError as e:
45
+ print(f"Error parsing config.yaml: {e}")
46
+ return None
47
+
48
+
49
+ def extract_paths(file_content):
50
+ """
51
+ Extracts API paths from a TypeScript paths file, including paths with parameters,
52
+ and derives the HTTP method from the key, with special handling for 'list'.
53
+
54
+ Parameters:
55
+ - file_content (str): The content of the TypeScript file containing the paths.
56
+
57
+ Returns:
58
+ - dict: A dictionary mapping path names to a tuple containing the HTTP method, API path, and additional parameters.
59
+ """
60
+ paths = {}
61
+ # Match the pattern for API paths (e.g., assemblyList: "/external-api/assemblies" or "/external-api/assemblies/:assemblyId")
62
+ matches = re.findall(r'(\w+):\s*"(/[\w/-]+(:\w+)?)"', file_content)
63
+
64
+ for match in matches:
65
+ path_name, api_path, _ = match
66
+
67
+ # Derive the method from the path name
68
+ method, requires_pagination = derive_http_method(path_name)
69
+
70
+ # Store the derived method, the API path, and pagination info
71
+ paths[path_name] = {
72
+ 'method': method,
73
+ 'path': api_path,
74
+ 'pagination': requires_pagination
75
+ }
76
+
77
+ return paths
78
+
79
+
80
+ def derive_http_method(path_name):
81
+ """
82
+ Derives the HTTP method from the path name based on common action keywords,
83
+ and determines if pagination is needed.
84
+
85
+ Parameters:
86
+ - path_name (str): The name of the path (e.g., 'create', 'update', 'list', 'info').
87
+
88
+ Returns:
89
+ - tuple: (HTTP method as a string, requires_pagination as a bool)
90
+ """
91
+ # Common action keywords mapped to HTTP methods
92
+ action_method_map = {
93
+ 'create': 'POST',
94
+ 'update': 'PUT',
95
+ 'delete': 'DELETE',
96
+ 'remove': 'DELETE',
97
+ 'get': 'GET',
98
+ 'info': 'GET'
99
+ }
100
+
101
+ # Default to GET method
102
+ method = 'GET'
103
+ requires_pagination = False
104
+
105
+ # Special case for 'list' indicating a paginated GET request
106
+ if 'list' in path_name.lower():
107
+ method = 'GET'
108
+ requires_pagination = True
109
+
110
+ # Check for other keywords to determine the method
111
+ for keyword, mapped_method in action_method_map.items():
112
+ if keyword.lower() in path_name.lower():
113
+ method = mapped_method
114
+ break
115
+
116
+ return method, requires_pagination
117
+
118
+
119
+ def get_ts_files_info(base_dir):
120
+ """
121
+ Searches through the specified directory for paths files, extracts relevant information from them.
122
+
123
+ Parameters:
124
+ - base_dir (str): The base directory where the search will start.
125
+
126
+ Returns:
127
+ - list: A list of dictionaries containing extracted data from paths files.
128
+ """
129
+ extracted_data = []
130
+
131
+ # Traverse the directory to find TypeScript files
132
+ for root, dirs, files in os.walk(base_dir):
133
+ for file in files:
134
+ if file.endswith("paths.ts"):
135
+ file_path = os.path.join(root, file)
136
+ with open(file_path, 'r', encoding='utf-8') as f:
137
+ content = f.read()
138
+ paths_info = extract_paths(content)
139
+ extracted_data.append({
140
+ 'file': file,
141
+ 'type': 'paths',
142
+ 'data': paths_info
143
+ })
144
+
145
+ return extracted_data
146
+
147
+
148
+
149
+ def extract_urls_from_file(file_path):
150
+ """
151
+ Extract all URLs from the specified api_client.py file.
152
+
153
+ Parameters:
154
+ - file_path (str): The path to the api_client.py file.
155
+
156
+ Returns:
157
+ - list: A list of all extracted URLs in the form they appear in the file.
158
+ """
159
+ urls = []
160
+ # Define a regex pattern to match URL strings in the format "/external-api/...".
161
+ url_pattern = re.compile(r'url\s*=\s*f?"(/external-api/[^\s"]+)"')
162
+
163
+ with open(file_path, 'r', encoding='utf-8') as file:
164
+ # Read the entire content of the file.
165
+ content = file.read()
166
+
167
+ # Find all occurrences of the URL pattern.
168
+ matches = url_pattern.findall(content)
169
+
170
+ # Collect and clean the URLs.
171
+ for match in matches:
172
+ urls.append(match)
173
+
174
+ return urls
175
+
176
+ def find_new_apis(extracted_info, existing_urls):
177
+ """
178
+ Identifies APIs from extracted TypeScript paths that are not yet implemented in the Python client.
179
+
180
+ Parameters:
181
+ - extracted_info (list): List of dictionaries containing data from paths files.
182
+ - existing_urls (list): List of URLs extracted from the existing Python API client.
183
+
184
+ Returns:
185
+ - list: A list of new APIs that need to be created.
186
+ """
187
+ new_apis = []
188
+
189
+ # Normalize the existing URLs to match the TypeScript format
190
+ normalized_existing_urls = {url.replace('{',':').replace('}','').rstrip('/') for url in existing_urls}
191
+
192
+ # Loop through each extracted TypeScript path
193
+ for file in extracted_info:
194
+ for path_name, path_data in file['data'].items():
195
+ # Normalize the path to ensure consistent comparison
196
+ normalized_path = path_data['path'].rstrip('/')
197
+
198
+ # Check if the path exists in the Python client
199
+ if normalized_path not in normalized_existing_urls:
200
+ new_apis.append({
201
+ 'name': path_name,
202
+ 'path': normalized_path,
203
+ 'method': path_data.get('method', 'GET'),
204
+ 'pagination': path_data.get('pagination', False)
205
+ })
206
+
207
+ return new_apis
208
+
209
+ def identify_missing_external_apis(base_directory, python_client_path):
210
+ """
211
+ Wrapper function to extract, compare, and identify missing API implementations.
212
+
213
+ Parameters:
214
+ - base_directory (str): The base directory where the TypeScript paths files are located.
215
+ - python_client_path (str): The path to the Python client file containing the implemented URLs.
216
+
217
+ Returns:
218
+ - list: A list of new APIs that need to be implemented.
219
+
220
+ Eaxmple:
221
+ base_directory = C:/Users/Sean.Nicholson/GitHub/core2/backend/src/modules/external_api
222
+ python_client_path = ./external/api_client.py
223
+
224
+ """
225
+ # Extract TypeScript path info
226
+ extracted_info = get_ts_files_info(base_directory)
227
+
228
+ # Extract URLs from the existing Python client
229
+ existing_urls = extract_urls_from_file(python_client_path)
230
+
231
+ # Find new APIs that are not yet implemented
232
+ new_apis = find_new_apis(extracted_info, existing_urls)
233
+
234
+ # Print the new APIs for clarity
235
+ for api in new_apis:
236
+ print(f"API Name: {api['name']}, Method: {api['method']}, Path: {api['path']}, Pagination: {api['pagination']}")
237
+
238
+ return new_apis