drivelinepy 1.5.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.
- drivelinepy-1.5.1/LICENSE +21 -0
- drivelinepy-1.5.1/PKG-INFO +34 -0
- drivelinepy-1.5.1/README.md +16 -0
- drivelinepy-1.5.1/drivelinepy/__init__.py +6 -0
- drivelinepy-1.5.1/drivelinepy/base_api_wrapper.py +184 -0
- drivelinepy-1.5.1/drivelinepy/brightpearl_api.py +176 -0
- drivelinepy-1.5.1/drivelinepy/brightpearl_utils/__init__.py +0 -0
- drivelinepy-1.5.1/drivelinepy/brightpearl_utils/helper_functions.py +27 -0
- drivelinepy-1.5.1/drivelinepy/slack_api.py +99 -0
- drivelinepy-1.5.1/drivelinepy/traq_api.py +255 -0
- drivelinepy-1.5.1/drivelinepy/virtuagym_api.py +178 -0
- drivelinepy-1.5.1/drivelinepy/zoho_billing_api.py +406 -0
- drivelinepy-1.5.1/drivelinepy/zoho_crm_api.py +248 -0
- drivelinepy-1.5.1/drivelinepy.egg-info/PKG-INFO +34 -0
- drivelinepy-1.5.1/drivelinepy.egg-info/SOURCES.txt +18 -0
- drivelinepy-1.5.1/drivelinepy.egg-info/dependency_links.txt +1 -0
- drivelinepy-1.5.1/drivelinepy.egg-info/requires.txt +2 -0
- drivelinepy-1.5.1/drivelinepy.egg-info/top_level.txt +1 -0
- drivelinepy-1.5.1/setup.cfg +4 -0
- drivelinepy-1.5.1/setup.py +27 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Driveline Research and Development
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: drivelinepy
|
|
3
|
+
Version: 1.5.1
|
|
4
|
+
Summary: A Python package for Driveline Baseball API interactions
|
|
5
|
+
Home-page: https://github.com/drivelineresearch/drivelinepy
|
|
6
|
+
Author: Garrett York
|
|
7
|
+
Author-email: garrett@drivelinebaseball.com
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.6.6
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
|
|
17
|
+
# Drivelinepy
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install drivelinepy
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
[Using Environment Variables](/docs/env-variables.md).
|
|
26
|
+
|
|
27
|
+
## System Specific Docs
|
|
28
|
+
- [Slack API](/docs/slack-api.md)
|
|
29
|
+
- [TRAQ API](/docs/traq-api.md)
|
|
30
|
+
- [Virtuagym API](/docs/virtuagym-api.md)
|
|
31
|
+
- [Zoho APIs](/docs/zoho-overview.md)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Drivelinepy
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install drivelinepy
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
[Using Environment Variables](/docs/env-variables.md).
|
|
10
|
+
|
|
11
|
+
## System Specific Docs
|
|
12
|
+
- [Slack API](/docs/slack-api.md)
|
|
13
|
+
- [TRAQ API](/docs/traq-api.md)
|
|
14
|
+
- [Virtuagym API](/docs/virtuagym-api.md)
|
|
15
|
+
- [Zoho APIs](/docs/zoho-overview.md)
|
|
16
|
+
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#=================================================================
|
|
2
|
+
# Author: Garrett York
|
|
3
|
+
# Date: 2023/11/29
|
|
4
|
+
# Description: Boiler plate code for API wrapper
|
|
5
|
+
#=================================================================
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import time
|
|
11
|
+
from urllib.parse import urljoin
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
class BaseAPIWrapper():
|
|
15
|
+
|
|
16
|
+
#-----------------------------------------------------------------
|
|
17
|
+
# Constructor
|
|
18
|
+
#-----------------------------------------------------------------
|
|
19
|
+
def __init__(self, base_url, auth_url=None):
|
|
20
|
+
self.base_url = base_url
|
|
21
|
+
self.auth_url = auth_url
|
|
22
|
+
self.logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
#-----------------------------------------------------------------
|
|
25
|
+
# Method - Construct URL
|
|
26
|
+
#-----------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def _url(self, path, is_auth=False):
|
|
29
|
+
"""Construct URL from base/auth url and path."""
|
|
30
|
+
if is_auth:
|
|
31
|
+
url = urljoin(self.auth_url, path)
|
|
32
|
+
else:
|
|
33
|
+
url = urljoin(self.base_url, path)
|
|
34
|
+
return url
|
|
35
|
+
|
|
36
|
+
#-----------------------------------------------------------------
|
|
37
|
+
# Method - Prepare Parameters
|
|
38
|
+
#-----------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
def _prepare_params(self, base_params, additional_params=None):
|
|
41
|
+
"""
|
|
42
|
+
Prepares and merges base parameters with additional parameters.
|
|
43
|
+
|
|
44
|
+
:param base_params: A dictionary of base parameters common to all API requests.
|
|
45
|
+
:param additional_params: A dictionary of additional parameters specific to a particular API request.
|
|
46
|
+
:return: A merged dictionary of parameters with none values filtered out.
|
|
47
|
+
"""
|
|
48
|
+
self.logger.info(f"Entering _prepare_params() with base parameters: {base_params}")
|
|
49
|
+
|
|
50
|
+
if base_params is None:
|
|
51
|
+
base_params = {}
|
|
52
|
+
params = base_params.copy()
|
|
53
|
+
|
|
54
|
+
if additional_params is not None:
|
|
55
|
+
params.update(additional_params) # Merges additional parameters
|
|
56
|
+
|
|
57
|
+
# Filtering out None values
|
|
58
|
+
filtered_params = {k: v for k, v in params.items() if v is not None}
|
|
59
|
+
|
|
60
|
+
self.logger.info("Exiting _prepare_params()")
|
|
61
|
+
return filtered_params
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
#-----------------------------------------------------------------
|
|
65
|
+
# Method - Send Request
|
|
66
|
+
#-----------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def _send_request(self, method, path, params=None, data=None, headers=None, is_auth=False, files=None):
|
|
69
|
+
"""Send a request to the API."""
|
|
70
|
+
url = self._url(path, is_auth=is_auth)
|
|
71
|
+
|
|
72
|
+
# Prepare the keyword arguments for the request method.
|
|
73
|
+
request_kwargs = {
|
|
74
|
+
'params': params,
|
|
75
|
+
'headers': headers,
|
|
76
|
+
'data': data,
|
|
77
|
+
'files': files
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Make sure not to send 'None' values
|
|
81
|
+
request_kwargs = {k: v for k, v in request_kwargs.items() if v is not None}
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# Make a single request with the conditional arguments.
|
|
85
|
+
response = requests.request(method, url, **request_kwargs)
|
|
86
|
+
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
return response.json()
|
|
89
|
+
except requests.exceptions.HTTPError as err:
|
|
90
|
+
self.logger.error(f'HTTP error occurred: {err}')
|
|
91
|
+
except requests.exceptions.RequestException as req_err:
|
|
92
|
+
self.logger.error(f'Request error: {req_err}')
|
|
93
|
+
except Exception as err:
|
|
94
|
+
self.logger.error(f'An unexpected error occurred: {err}')
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
#-----------------------------------------------------------------
|
|
98
|
+
# Method - HTTP Functions
|
|
99
|
+
#-----------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def get(self, path, params=None, headers=None):
|
|
102
|
+
"""GET request."""
|
|
103
|
+
return self._send_request('GET', path, params=params, headers=headers)
|
|
104
|
+
|
|
105
|
+
def post(self, path, data=None, headers=None, is_auth=False, files=None):
|
|
106
|
+
"""POST request."""
|
|
107
|
+
return self._send_request('POST', path, data=data, headers=headers, is_auth=is_auth, files=files)
|
|
108
|
+
|
|
109
|
+
def put(self, path, data, headers=None):
|
|
110
|
+
"""PUT request."""
|
|
111
|
+
return self._send_request('PUT', path, data=data, headers=headers)
|
|
112
|
+
|
|
113
|
+
def options(self, path, headers=None):
|
|
114
|
+
"""OPTIONS request."""
|
|
115
|
+
return self._send_request('OPTIONS', path, headers=headers)
|
|
116
|
+
|
|
117
|
+
def delete(self, path, headers=None):
|
|
118
|
+
"""DELETE request."""
|
|
119
|
+
return self._send_request('DELETE', path, headers=headers)
|
|
120
|
+
|
|
121
|
+
#-----------------------------------------------------------------
|
|
122
|
+
# Method - Convert Date to Timestamp
|
|
123
|
+
#-----------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def convert_yyyy_mm_dd_to_timestamp(self, date_str, is_end_date=False):
|
|
126
|
+
"""
|
|
127
|
+
Convert a date string to a Unix timestamp.
|
|
128
|
+
|
|
129
|
+
:param date_str: Date string in 'YYYY-MM-DD' format.
|
|
130
|
+
:param is_end_date: Boolean indicating whether the date is an end date.
|
|
131
|
+
If True, it will set the time to the end of the day.
|
|
132
|
+
:return: Unix timestamp corresponding to the provided date.
|
|
133
|
+
"""
|
|
134
|
+
self.logger.info(f'Entering convert_yyyy_mm_dd_to_timestamp() for date {date_str}')
|
|
135
|
+
|
|
136
|
+
if is_end_date:
|
|
137
|
+
date_time_str = date_str + " 23:59:59.999999"
|
|
138
|
+
else:
|
|
139
|
+
date_time_str = date_str + " 00:00:00.000000"
|
|
140
|
+
|
|
141
|
+
# Converting to datetime object
|
|
142
|
+
date_time_obj = datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S.%f')
|
|
143
|
+
# Converting to Unix timestamp
|
|
144
|
+
timestamp = time.mktime(date_time_obj.timetuple())
|
|
145
|
+
self.logger.info("Exiting convert_yyyy_mm_dd_to_timestamp()")
|
|
146
|
+
return timestamp
|
|
147
|
+
|
|
148
|
+
#-----------------------------------------------------------------
|
|
149
|
+
# Method - Validate Date Format
|
|
150
|
+
#-----------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
def validate_date_yyyy_mm_dd(self, date_str):
|
|
153
|
+
"""
|
|
154
|
+
Validate a date string in 'YYYY-MM-DD' format.
|
|
155
|
+
|
|
156
|
+
:param date_str: Date string in 'YYYY-MM-DD' format.
|
|
157
|
+
:return: Boolean indicating whether the date string is valid.
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
datetime.strptime(date_str, '%Y-%m-%d')
|
|
161
|
+
self.logger.info(f"Date string is valid: {date_str}. Exiting validate_date_yyyy_mm_dd()")
|
|
162
|
+
return True
|
|
163
|
+
except ValueError:
|
|
164
|
+
self.logger.error(f"Incorrect data format for date string '{date_str}', should be YYYY-MM-DD. Exiting validate_date_yyyy_mm_dd()")
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
#-----------------------------------------------------------------
|
|
168
|
+
# Method - Validate Email
|
|
169
|
+
#-----------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def validate_email(self, email):
|
|
172
|
+
"""
|
|
173
|
+
Validates that the provided string is a valid email format.
|
|
174
|
+
|
|
175
|
+
:param email: The email address to be validated.
|
|
176
|
+
:return: Boolean indicating whether the email is valid.
|
|
177
|
+
"""
|
|
178
|
+
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
179
|
+
if re.match(email_regex, email):
|
|
180
|
+
self.logger.info(f"Email address {email} is valid.")
|
|
181
|
+
return True
|
|
182
|
+
else:
|
|
183
|
+
self.logger.error(f"Invalid email address: {email}.")
|
|
184
|
+
return False
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# =================================================================
|
|
2
|
+
# Author: Diego Plata
|
|
3
|
+
# Date: 2023/12/19
|
|
4
|
+
# Description: Class for Brightpearl API
|
|
5
|
+
# =================================================================
|
|
6
|
+
|
|
7
|
+
from base_api_wrapper import BaseAPIWrapper
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from brightpearl_utils.helper_functions import *
|
|
10
|
+
|
|
11
|
+
class BrightpearlApi(BaseAPIWrapper):
|
|
12
|
+
|
|
13
|
+
# -----------------------------------------------------------------
|
|
14
|
+
# Method - Constructor
|
|
15
|
+
# -----------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
def __init__(self, client_id, client_secret, refresh_token, api_iteration_limit=50,
|
|
18
|
+
auth_url="https://oauth.brightpearl.com", base_url=""):
|
|
19
|
+
super().__init__(base_url)
|
|
20
|
+
self.auth_url = auth_url
|
|
21
|
+
self.client_id = client_id
|
|
22
|
+
self.client_secret = client_secret
|
|
23
|
+
self.refresh_token = refresh_token
|
|
24
|
+
self.api_iteration_limit = api_iteration_limit # Number of times to call an API endpoint in a single function
|
|
25
|
+
self.time_of_last_refresh = None # Set in _refresh_access_token()
|
|
26
|
+
self.access_token = None # Set in _refresh_access_token()
|
|
27
|
+
self._refresh_access_token()
|
|
28
|
+
|
|
29
|
+
# -----------------------------------------------------------------
|
|
30
|
+
# Method - Refresh Access Token
|
|
31
|
+
# -----------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def _refresh_access_token(self):
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
Refreshes the OAuth access token using the refresh token.
|
|
37
|
+
|
|
38
|
+
:return: True if the access token was successfully refreshed, False otherwise.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
self.logger.info("Entering _refresh_access_token()")
|
|
42
|
+
self.time_of_last_refresh = datetime.now()
|
|
43
|
+
|
|
44
|
+
path = "/token/driveline"
|
|
45
|
+
|
|
46
|
+
data = {
|
|
47
|
+
'refresh_token': self.refresh_token,
|
|
48
|
+
'client_id': self.client_id,
|
|
49
|
+
'client_secret': self.client_secret,
|
|
50
|
+
'grant_type': 'refresh_token'
|
|
51
|
+
}
|
|
52
|
+
headers = {}
|
|
53
|
+
|
|
54
|
+
response = self.post(path=path, headers=headers, data=data, is_auth=True)
|
|
55
|
+
|
|
56
|
+
# valid if response is successful, if it was return true, if not return false
|
|
57
|
+
if response is None:
|
|
58
|
+
self.logger.error("Failed to refresh access token - response is None")
|
|
59
|
+
return False
|
|
60
|
+
else:
|
|
61
|
+
# if access token exists, return true
|
|
62
|
+
if "access_token" in response:
|
|
63
|
+
self.access_token = response["access_token"]
|
|
64
|
+
self.base_url = f"https://{response['api_domain']}"
|
|
65
|
+
self.logger.info("Access token refreshed and base url set to api domain")
|
|
66
|
+
return True
|
|
67
|
+
else:
|
|
68
|
+
self.logger.error("Failed to refresh access token - access token does not exist in response")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# -----------------------------------------------------------------
|
|
72
|
+
# Method - Check Last Time Access Token Was Refreshed
|
|
73
|
+
# -----------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def check_if_access_token_needs_refreshed(self):
|
|
76
|
+
"""
|
|
77
|
+
Checks if the access token needs to be refreshed.
|
|
78
|
+
return: True if the access token needs to be refreshed, False otherwise.
|
|
79
|
+
"""
|
|
80
|
+
self.logger.info("Entering check_last_refresh()")
|
|
81
|
+
|
|
82
|
+
time_since_last_refresh = datetime.now() - self.time_of_last_refresh
|
|
83
|
+
|
|
84
|
+
if time_since_last_refresh.seconds > 3600:
|
|
85
|
+
self.logger.info("Exiting check_last_refresh() - Token needs to be refreshed")
|
|
86
|
+
return True
|
|
87
|
+
else:
|
|
88
|
+
self.logger.info("Exiting check_last_refresh() - Token does not need to be refreshed")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
# -----------------------------------------------------------------
|
|
92
|
+
# Method - Get Headers
|
|
93
|
+
# -----------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def _get_headers(self):
|
|
96
|
+
self.logger.info("Entering _get_headers()")
|
|
97
|
+
if self.check_if_access_token_needs_refreshed():
|
|
98
|
+
if not self._refresh_access_token():
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
headers = {
|
|
102
|
+
'Authorization': f"Bearer {self.access_token}",
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
'brightpearl-app-ref': self.client_id,
|
|
105
|
+
'brightpearl-dev-ref': 'driveline1058'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
self.logger.info(f'Headers have been set!')
|
|
109
|
+
self.logger.info("Exiting _get_headers()")
|
|
110
|
+
return headers
|
|
111
|
+
|
|
112
|
+
def get_all_sales_orders_from_date(self, since_date, page_size):
|
|
113
|
+
# TODO: add docstring documentation
|
|
114
|
+
self.logger.info("Entering get_all_sales_orders()")
|
|
115
|
+
endpoint = "/public-api/driveline/order-service/sales-order-search"
|
|
116
|
+
|
|
117
|
+
result_index = 1
|
|
118
|
+
today = pd.to_datetime('today').date()
|
|
119
|
+
params = {
|
|
120
|
+
"createdOn": f"{since_date}T00:00/{today}T00:00",
|
|
121
|
+
"pageSize": page_size,
|
|
122
|
+
"firstResult": result_index
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
sales_orders = self._fetch_sales_orders_by_date(self.base_url+endpoint, params)
|
|
126
|
+
|
|
127
|
+
return sales_orders
|
|
128
|
+
|
|
129
|
+
def get_all_sales_orders_by_ids(self, sales_orders):
|
|
130
|
+
# TODO: add docstring documentation
|
|
131
|
+
self.logger.info("Entering get_all_sales_orders_by_id()")
|
|
132
|
+
order_service_endpoint = "/public-api/driveline/order-service"
|
|
133
|
+
sales_order_endpoint = "/public-api/driveline/order-service/sales-order"
|
|
134
|
+
params = {
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
headers = self._get_headers()
|
|
138
|
+
orders_ids = construct_sales_orders_url(sales_orders['salesOrderId'].values, self.base_url)
|
|
139
|
+
uris_response = self.options(f"{self.base_url}{sales_order_endpoint}/{','.join(orders_ids)}", headers=headers)
|
|
140
|
+
urls = uris_response["response"]["getUris"]
|
|
141
|
+
sales_orders = self._fetch_sales_orders_by_ids(f"{self.base_url}{order_service_endpoint}", params, urls)
|
|
142
|
+
|
|
143
|
+
return sales_orders
|
|
144
|
+
|
|
145
|
+
def _fetch_sales_orders_by_date(self, endpoint, initial_params):
|
|
146
|
+
# TODO: add docstring documentation
|
|
147
|
+
self.logger.info(f'Entering _fetch_data() for endpoint {endpoint}')
|
|
148
|
+
all_data_list = []
|
|
149
|
+
params = initial_params
|
|
150
|
+
headers = self._get_headers()
|
|
151
|
+
more_pages_available = True
|
|
152
|
+
while more_pages_available:
|
|
153
|
+
response = self.get(endpoint, params=params, headers=headers)
|
|
154
|
+
more_pages_available = response["response"]["metaData"]["morePagesAvailable"]
|
|
155
|
+
data_count = response["response"]["metaData"]["resultsReturned"]
|
|
156
|
+
first_index_returned = response["response"]["metaData"]["firstResult"]
|
|
157
|
+
last_index_returned = response["response"]["metaData"]["lastResult"]
|
|
158
|
+
self.logger.info(f"Retrieved {data_count} results from index {first_index_returned} to index {last_index_returned}")
|
|
159
|
+
all_data_list.append(sales_orders_list_to_dataframe(response["response"]["results"], response["response"]["metaData"]["columns"]))
|
|
160
|
+
result_index = response["response"]["metaData"]["lastResult"] + 1
|
|
161
|
+
params['firstResult'] = result_index
|
|
162
|
+
|
|
163
|
+
self.logger.info(f"Exiting _fetch_sales_orders_by_date()")
|
|
164
|
+
return pd.concat(all_data_list)
|
|
165
|
+
|
|
166
|
+
def _fetch_sales_orders_by_ids(self, endpoint, initial_params, urls):
|
|
167
|
+
# TODO: add docstring documentation
|
|
168
|
+
self.logger.info(f'Entering _fetch_data() for endpoint {endpoint}')
|
|
169
|
+
params = initial_params
|
|
170
|
+
headers = self._get_headers()
|
|
171
|
+
response = [self.get(endpoint+url, params=params, headers=headers) for url in urls]
|
|
172
|
+
individual_sales_orders_list = list()
|
|
173
|
+
for sale_order_dictionary in response:
|
|
174
|
+
individual_sales_orders_list.append(individual_sales_orders_to_dataframe(sale_order_dictionary["response"]))
|
|
175
|
+
self.logger.info(f"Exiting _fetch_sales_orders_by_ids()")
|
|
176
|
+
return pd.concat(individual_sales_orders_list)
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def sales_orders_list_to_dataframe(results, columns):
|
|
5
|
+
header_columns = [column["name"] for column in columns]
|
|
6
|
+
return pd.DataFrame(results, columns=header_columns)
|
|
7
|
+
|
|
8
|
+
def construct_sales_orders_url(ids, base_url):
|
|
9
|
+
string_ids = [str(id) for id in ids]
|
|
10
|
+
return string_ids
|
|
11
|
+
|
|
12
|
+
def individual_sales_orders_to_dataframe(orders):
|
|
13
|
+
return pd.json_normalize(orders, sep='_')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def append_order_and_individual_sales_entry(orders):
|
|
17
|
+
row_frames = list()
|
|
18
|
+
for _, row in orders.iterrows():
|
|
19
|
+
row_frame = pd.json_normalize(row["rows"])
|
|
20
|
+
# can join later
|
|
21
|
+
row_frame["salesOrderId"] = row["id"]
|
|
22
|
+
row_frame = row_frame.add_prefix("individual_sale_entry_")
|
|
23
|
+
row_frames.append(row_frame)
|
|
24
|
+
row_frames = pd.concat(row_frames)
|
|
25
|
+
megaframe = orders.merge(row_frames, how='right',left_on=["id"], right_on=["individual_sale_entry_salesOrderId"], suffixes=("","sale_entry"))
|
|
26
|
+
megaframe = megaframe.drop("rows",axis=1)
|
|
27
|
+
return megaframe
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#================================================================================
|
|
2
|
+
# Author: Garrett York
|
|
3
|
+
# Date: 2024/02/01
|
|
4
|
+
# Description: Class for Slack API
|
|
5
|
+
#================================================================================
|
|
6
|
+
|
|
7
|
+
from .base_api_wrapper import BaseAPIWrapper
|
|
8
|
+
import os
|
|
9
|
+
import mimetypes
|
|
10
|
+
|
|
11
|
+
class SlackAPI(BaseAPIWrapper):
|
|
12
|
+
|
|
13
|
+
#---------------------------------------------------------------------------
|
|
14
|
+
# Constructor
|
|
15
|
+
#---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
def __init__(self, token, base_url="https://slack.com/api/"):
|
|
18
|
+
super().__init__(base_url)
|
|
19
|
+
self.token = token
|
|
20
|
+
|
|
21
|
+
#---------------------------------------------------------------------------
|
|
22
|
+
# Method - Post Message
|
|
23
|
+
#---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def post_message(self, channel, text):
|
|
26
|
+
"""
|
|
27
|
+
Posts a message to a specified channel on Slack.
|
|
28
|
+
|
|
29
|
+
:param channel: The channel ID where the message will be posted.
|
|
30
|
+
:param text: The text of the message to post.
|
|
31
|
+
:return: The response from the Slack API.
|
|
32
|
+
"""
|
|
33
|
+
self.logger.info("Entering post_message()")
|
|
34
|
+
|
|
35
|
+
endpoint = "chat.postMessage"
|
|
36
|
+
payload = {
|
|
37
|
+
'channel': channel,
|
|
38
|
+
'text': text
|
|
39
|
+
}
|
|
40
|
+
headers = {
|
|
41
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
42
|
+
'Authorization': f'Bearer {self.token}'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
response = self.post(endpoint, data=payload, headers=headers)
|
|
46
|
+
|
|
47
|
+
self.logger.info("Exiting post_message()")
|
|
48
|
+
return response
|
|
49
|
+
|
|
50
|
+
#---------------------------------------------------------------------------
|
|
51
|
+
# Method - Post File + Message (optional)
|
|
52
|
+
#---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def upload_file(self, channel, file_absolute_path, text=None):
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
Uploads a file to a specified channel on Slack.
|
|
58
|
+
|
|
59
|
+
This method attempts to upload a file to the specified Slack channel. It first checks if the file exists at the given path and then determines its MIME type for proper uploading. The file is then uploaded with an optional initial comment.
|
|
60
|
+
|
|
61
|
+
:param channel: str
|
|
62
|
+
The channel ID where the file will be uploaded.
|
|
63
|
+
:param file_absolute_path: str
|
|
64
|
+
The absolute path to the file to upload.
|
|
65
|
+
:param text: str, optional
|
|
66
|
+
An initial comment to add when uploading the file. Defaults to None. The API documentation refers to this parameter is initial_comment
|
|
67
|
+
but it is referred to as text in this method to match the parameter name in the post_message method.
|
|
68
|
+
|
|
69
|
+
:return: dict
|
|
70
|
+
A dictionary response from the Slack API indicating the success or failure of the file upload.
|
|
71
|
+
"""
|
|
72
|
+
self.logger.info("Entering upload_file()")
|
|
73
|
+
|
|
74
|
+
if not os.path.exists(file_absolute_path):
|
|
75
|
+
self.logger.error(f"File not found: {file_absolute_path}")
|
|
76
|
+
return {"ok": False, "error": "file_not_found"}
|
|
77
|
+
|
|
78
|
+
endpoint = "files.upload"
|
|
79
|
+
|
|
80
|
+
# Attempt to determine the MIME type of the file based on its extension.
|
|
81
|
+
# The `guess_type` function returns a tuple (MIME type, encoding),
|
|
82
|
+
# where we are only interested in the MIME type. The underscore (_)
|
|
83
|
+
# is used to ignore the encoding part of the returned tuple.
|
|
84
|
+
|
|
85
|
+
mime_type, _ = mimetypes.guess_type(file_absolute_path)
|
|
86
|
+
mime_type = mime_type or 'application/octet-stream'
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
with open(file_absolute_path, 'rb') as file_content:
|
|
90
|
+
files = [('file', (os.path.basename(file_absolute_path), file_content, mime_type))]
|
|
91
|
+
payload = {'channels': channel, 'initial_comment': text}
|
|
92
|
+
headers = {'Authorization': f'Bearer {self.token}'}
|
|
93
|
+
response = self.post(endpoint, headers=headers, data=payload, files=files)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
self.logger.error(f"Error uploading file: {e}")
|
|
96
|
+
return {"ok": False, "error": str(e)}
|
|
97
|
+
|
|
98
|
+
self.logger.info("Exiting upload_file()")
|
|
99
|
+
return response
|