cit-pydata 0.0.2.dev4__tar.gz → 0.0.2.dev6__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.
- {cit_pydata-0.0.2.dev4/src/cit_pydata.egg-info → cit_pydata-0.0.2.dev6}/PKG-INFO +5 -1
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/pyproject.toml +3 -2
- cit_pydata-0.0.2.dev6/src/cit_pydata/mailer/__init__.py +1 -0
- cit_pydata-0.0.2.dev6/src/cit_pydata/mailer/api.py +280 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6/src/cit_pydata.egg-info}/PKG-INFO +5 -1
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata.egg-info/SOURCES.txt +2 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata.egg-info/requires.txt +5 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/LICENSE +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/README.md +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/setup.cfg +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/aws/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/aws/api.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/box/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/box/api.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/joblog/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/joblog/api.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/marketo/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/marketo/api.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/salesforce/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/salesforce/api.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/sfsync/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/sfsync/api.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/sftp/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/sftp/api.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/sql/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/sql/api.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/util/__init__.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata/util/api.py +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata.egg-info/dependency_links.txt +0 -0
- {cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cit-pydata
|
|
3
|
-
Version: 0.0.2.
|
|
3
|
+
Version: 0.0.2.dev6
|
|
4
4
|
Summary: Python clients for commonly used api services including Salesforce, SQL, Marketo, Box, SFSync*, and JobLog*
|
|
5
5
|
Author-email: Dionis Wang <dwang@eab.com>, Andrew Fitch <afitch@eab.com>, Dagim Desta <ddesta@eab.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/EAB-IT-DEV/cit-pydata
|
|
@@ -18,6 +18,9 @@ Provides-Extra: box
|
|
|
18
18
|
Requires-Dist: boxsdk; extra == "box"
|
|
19
19
|
Provides-Extra: joblog
|
|
20
20
|
Requires-Dist: marketorestpython; extra == "joblog"
|
|
21
|
+
Provides-Extra: mailer
|
|
22
|
+
Requires-Dist: requests; extra == "mailer"
|
|
23
|
+
Requires-Dist: boto3; extra == "mailer"
|
|
21
24
|
Provides-Extra: salesforce
|
|
22
25
|
Requires-Dist: simple_salesforce; extra == "salesforce"
|
|
23
26
|
Requires-Dist: pyforce; extra == "salesforce"
|
|
@@ -70,6 +73,7 @@ Requires-Dist: pandas; extra == "all"
|
|
|
70
73
|
Requires-Dist: psycopg2-binary; extra == "all"
|
|
71
74
|
Requires-Dist: python-dotenv; extra == "all"
|
|
72
75
|
Requires-Dist: boto3; extra == "all"
|
|
76
|
+
Requires-Dist: requests; extra == "all"
|
|
73
77
|
Dynamic: license-file
|
|
74
78
|
|
|
75
79
|
# cit-pydata
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cit-pydata"
|
|
7
|
-
version = "0.0.2.
|
|
7
|
+
version = "0.0.2.dev6"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Dionis Wang", email="dwang@eab.com" },{ name="Andrew Fitch", email="afitch@eab.com" },{ name="Dagim Desta", email="ddesta@eab.com" }
|
|
10
10
|
]
|
|
@@ -24,6 +24,7 @@ Issues = "https://github.com/EAB-IT-DEV/cit-pydata/issues"
|
|
|
24
24
|
aws = ["aws-lambda-powertools", "boto3"]
|
|
25
25
|
box = ["boxsdk"]
|
|
26
26
|
joblog = ["marketorestpython"]
|
|
27
|
+
mailer = ["requests", "boto3"]
|
|
27
28
|
salesforce = ["simple_salesforce","pyforce"]
|
|
28
29
|
sftp = ["paramiko"]
|
|
29
30
|
sql = ["SQLAlchemy<2.0", "numpy", "pandas"]
|
|
@@ -33,4 +34,4 @@ sql-psycopg2 = ["SQLAlchemy<2.0", "psycopg2-binary", "numpy", "pandas"]
|
|
|
33
34
|
sql-mssql = ["SQLAlchemy<2.0", "pymssql", "numpy", "pandas"]
|
|
34
35
|
sql-postgres = ["SQLAlchemy<2.0", "psycopg2-binary", "numpy", "pandas"]
|
|
35
36
|
local = ["python-dotenv"]
|
|
36
|
-
all = ["aws-lambda-powertools","pandas","boxsdk","marketorestpython","simple_salesforce","pyforce","paramiko","SQLAlchemy<2.0", "pyodbc","pymssql", "numpy", "pandas","psycopg2-binary","python-dotenv", "boto3"]
|
|
37
|
+
all = ["aws-lambda-powertools","pandas","boxsdk","marketorestpython","simple_salesforce","pyforce","paramiko","SQLAlchemy<2.0", "pyodbc","pymssql", "numpy", "pandas","psycopg2-binary","python-dotenv", "boto3", "requests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .api import MailClient
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from cit_pydata.util import api as util_api
|
|
6
|
+
from cit_pydata.aws import api as aws_api
|
|
7
|
+
|
|
8
|
+
EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
|
9
|
+
FILE_ALIAS_PATTERN = re.compile(r"^[a-zA-Z0-9_-][a-zA-Z0-9_. -]*[a-zA-Z0-9_-]$")
|
|
10
|
+
|
|
11
|
+
GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0"
|
|
12
|
+
# Attachments above this size require a Graph upload session rather than an
|
|
13
|
+
# inline base64 fileAttachment. sendMail supports inline attachments up to ~3 MB.
|
|
14
|
+
MAX_INLINE_ATTACHMENT_BYTES = 3 * 1024 * 1024
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MailClient:
|
|
18
|
+
"""
|
|
19
|
+
Sends email via the Microsoft Graph API (sendMail endpoint).
|
|
20
|
+
|
|
21
|
+
Authenticates with the OAuth2 client-credentials flow using an app
|
|
22
|
+
registration (tenant_id / client_id / client_secret) that has been granted
|
|
23
|
+
the application permission "Mail.Send". Those credentials are read from AWS
|
|
24
|
+
SSM Parameter Store as a JSON document, e.g.:
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"tenant_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
28
|
+
"client_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
29
|
+
"client_secret": "xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
A single MailClient can be reused to send multiple emails; the access token
|
|
33
|
+
is fetched lazily on first send and reused.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
ssm_parameter_name: str = "/eab-pydata/graph/config",
|
|
39
|
+
environment: str = None,
|
|
40
|
+
iam_user: str = None,
|
|
41
|
+
approved_senders: list = None,
|
|
42
|
+
logger=None,
|
|
43
|
+
):
|
|
44
|
+
self.logger = util_api.get_logger(__name__, "INFO") if not logger else logger
|
|
45
|
+
|
|
46
|
+
self.ssm_parameter_name = ssm_parameter_name
|
|
47
|
+
|
|
48
|
+
# Only needed for local (non-AWS) auth; on AWS the execution role is used.
|
|
49
|
+
self.aws_environment = environment or util_api.get_environment_variable(
|
|
50
|
+
logger=self.logger, variable_name="aws_auth_environment"
|
|
51
|
+
)
|
|
52
|
+
self.aws_iam_user = iam_user or util_api.get_environment_variable(
|
|
53
|
+
logger=self.logger, variable_name="aws_auth_iam_user"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# When set, sender_email must match one of these (case-insensitive). None skips the check.
|
|
57
|
+
self.approved_senders = (
|
|
58
|
+
[s.lower() for s in approved_senders] if approved_senders else None
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Fetched lazily on first send.
|
|
62
|
+
self._access_token = None
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def is_valid_email(email: str) -> bool:
|
|
66
|
+
return EMAIL_PATTERN.match(email) is not None
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def is_valid_file_alias(alias: str) -> bool:
|
|
70
|
+
return FILE_ALIAS_PATTERN.match(alias) is not None
|
|
71
|
+
|
|
72
|
+
def _validate_email_list(self, email_list: list):
|
|
73
|
+
invalid = [e for e in email_list if not self.is_valid_email(e.strip())]
|
|
74
|
+
if invalid:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"The following email addresses are invalid: {', '.join(invalid)}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _check_sender(self, sender_email: str):
|
|
80
|
+
if not self.is_valid_email(sender_email):
|
|
81
|
+
raise ValueError(f'Sender email "{sender_email}" is not a valid email')
|
|
82
|
+
if (
|
|
83
|
+
self.approved_senders is not None
|
|
84
|
+
and sender_email.lower() not in self.approved_senders
|
|
85
|
+
):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f'Sender email "{sender_email}" is not in the approved sender list'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _get_graph_config(self) -> dict:
|
|
91
|
+
"""Reads the Graph app-registration credentials JSON from SSM."""
|
|
92
|
+
import json
|
|
93
|
+
|
|
94
|
+
ssm_client = aws_api.SSMClient(
|
|
95
|
+
environment=self.aws_environment,
|
|
96
|
+
iam_user=self.aws_iam_user,
|
|
97
|
+
logger=self.logger,
|
|
98
|
+
)
|
|
99
|
+
raw = ssm_client.get_parameter(
|
|
100
|
+
name=self.ssm_parameter_name, with_decryption=True
|
|
101
|
+
)
|
|
102
|
+
if not raw:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f'Graph config not found in SSM at "{self.ssm_parameter_name}"'
|
|
105
|
+
)
|
|
106
|
+
config = json.loads(raw)
|
|
107
|
+
for key in ("tenant_id", "client_id", "client_secret"):
|
|
108
|
+
if not config.get(key):
|
|
109
|
+
raise ValueError(f'Graph config is missing required key "{key}"')
|
|
110
|
+
return config
|
|
111
|
+
|
|
112
|
+
def _authenticate(self):
|
|
113
|
+
"""Fetches and caches an OAuth2 access token via the client-credentials flow."""
|
|
114
|
+
import requests
|
|
115
|
+
|
|
116
|
+
if self._access_token is not None:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
config = self._get_graph_config()
|
|
120
|
+
token_url = (
|
|
121
|
+
f"https://login.microsoftonline.com/{config['tenant_id']}"
|
|
122
|
+
"/oauth2/v2.0/token"
|
|
123
|
+
)
|
|
124
|
+
payload = {
|
|
125
|
+
"grant_type": "client_credentials",
|
|
126
|
+
"client_id": config["client_id"],
|
|
127
|
+
"client_secret": config["client_secret"],
|
|
128
|
+
"scope": "https://graph.microsoft.com/.default",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
response = requests.post(token_url, data=payload)
|
|
132
|
+
try:
|
|
133
|
+
response.raise_for_status()
|
|
134
|
+
except Exception as e:
|
|
135
|
+
self.logger.error(f"Failed to authenticate to Microsoft Graph: {e}")
|
|
136
|
+
self.logger.error(response.text)
|
|
137
|
+
raise
|
|
138
|
+
|
|
139
|
+
self._access_token = response.json().get("access_token")
|
|
140
|
+
if not self._access_token:
|
|
141
|
+
raise ValueError("Microsoft Graph token response did not include an access_token")
|
|
142
|
+
|
|
143
|
+
def _build_attachment(self, attachment, attachment_alias: str = None) -> dict:
|
|
144
|
+
"""
|
|
145
|
+
Builds a Graph fileAttachment dict from a filepath (str) or an object
|
|
146
|
+
exposing to_csv() (e.g. a pandas DataFrame).
|
|
147
|
+
"""
|
|
148
|
+
if attachment_alias and not self.is_valid_file_alias(attachment_alias):
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f'Attachment alias "{attachment_alias}" uses an unexpected format'
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if isinstance(attachment, str):
|
|
154
|
+
if not os.path.exists(attachment):
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f'Attachment file could not be found at path "{attachment}"'
|
|
157
|
+
)
|
|
158
|
+
filename = (
|
|
159
|
+
attachment_alias + os.path.splitext(attachment)[1]
|
|
160
|
+
if attachment_alias
|
|
161
|
+
else os.path.basename(attachment)
|
|
162
|
+
)
|
|
163
|
+
with open(attachment, "rb") as file_obj:
|
|
164
|
+
content_bytes = file_obj.read()
|
|
165
|
+
elif hasattr(attachment, "to_csv"):
|
|
166
|
+
filename = f"{attachment_alias}.csv" if attachment_alias else "attachment.csv"
|
|
167
|
+
content_bytes = attachment.to_csv(index=False).encode("utf-8")
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
"attachment must be a filepath (str) or an object with a to_csv() "
|
|
171
|
+
"method (e.g. a pandas DataFrame)"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if len(content_bytes) > MAX_INLINE_ATTACHMENT_BYTES:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f'Attachment "{filename}" exceeds the {MAX_INLINE_ATTACHMENT_BYTES} '
|
|
177
|
+
"byte limit for inline Graph attachments"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
182
|
+
"name": filename,
|
|
183
|
+
"contentBytes": base64.b64encode(content_bytes).decode("ascii"),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _to_recipient_list(emails: str) -> list:
|
|
188
|
+
return [
|
|
189
|
+
{"emailAddress": {"address": e.strip()}}
|
|
190
|
+
for e in emails.split(",")
|
|
191
|
+
if e.strip()
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
def send_email(
|
|
195
|
+
self,
|
|
196
|
+
sender_email: str,
|
|
197
|
+
receiver_email: str,
|
|
198
|
+
subject: str,
|
|
199
|
+
body: str = None,
|
|
200
|
+
cc_email: str = None,
|
|
201
|
+
attachment=None,
|
|
202
|
+
attachment_alias: str = None,
|
|
203
|
+
body_type: str = "HTML",
|
|
204
|
+
save_to_sent_items: bool = False,
|
|
205
|
+
) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Sends an email via Microsoft Graph. Returns True on success, False on failure.
|
|
208
|
+
|
|
209
|
+
sender_email must be a mailbox the app registration is authorized to send
|
|
210
|
+
as. receiver_email/cc_email accept comma-separated addresses.
|
|
211
|
+
attachment can be a filepath (str) or an object with a to_csv() method
|
|
212
|
+
(e.g. a pandas DataFrame). attachment_alias renames the attachment to
|
|
213
|
+
something more recipient-friendly (e.g. "Contract_Details").
|
|
214
|
+
body_type is "HTML" or "Text".
|
|
215
|
+
"""
|
|
216
|
+
import requests
|
|
217
|
+
|
|
218
|
+
if body_type not in ("HTML", "Text"):
|
|
219
|
+
self.logger.error('body_type must be "HTML" or "Text"')
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
self._check_sender(sender_email)
|
|
224
|
+
self._validate_email_list(receiver_email.split(","))
|
|
225
|
+
if cc_email:
|
|
226
|
+
self._validate_email_list(cc_email.split(","))
|
|
227
|
+
if not subject:
|
|
228
|
+
raise ValueError("subject cannot be empty")
|
|
229
|
+
except ValueError as e:
|
|
230
|
+
self.logger.error(e)
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
message = {
|
|
234
|
+
"subject": subject,
|
|
235
|
+
"body": {
|
|
236
|
+
"contentType": body_type,
|
|
237
|
+
"content": body or "-No message was included by the sender-",
|
|
238
|
+
},
|
|
239
|
+
"toRecipients": self._to_recipient_list(receiver_email),
|
|
240
|
+
}
|
|
241
|
+
if cc_email:
|
|
242
|
+
message["ccRecipients"] = self._to_recipient_list(cc_email)
|
|
243
|
+
|
|
244
|
+
attachment_label = None
|
|
245
|
+
if attachment is not None:
|
|
246
|
+
try:
|
|
247
|
+
file_attachment = self._build_attachment(attachment, attachment_alias)
|
|
248
|
+
except ValueError as e:
|
|
249
|
+
self.logger.error(e)
|
|
250
|
+
return False
|
|
251
|
+
message["attachments"] = [file_attachment]
|
|
252
|
+
attachment_label = file_attachment["name"]
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
self._authenticate()
|
|
256
|
+
except Exception:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
self.logger.info(
|
|
260
|
+
f"Preparing to send email via Graph: subject={subject}, sender={sender_email}, "
|
|
261
|
+
f"receiver={receiver_email}, cc={cc_email}, attachment={attachment_label}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
send_mail_url = f"{GRAPH_BASE_URL}/users/{sender_email}/sendMail"
|
|
265
|
+
headers = {
|
|
266
|
+
"Authorization": f"Bearer {self._access_token}",
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
}
|
|
269
|
+
payload = {"message": message, "saveToSentItems": save_to_sent_items}
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
response = requests.post(send_mail_url, headers=headers, json=payload)
|
|
273
|
+
response.raise_for_status()
|
|
274
|
+
self.logger.info("Email sent successfully")
|
|
275
|
+
return True
|
|
276
|
+
except Exception as e:
|
|
277
|
+
self.logger.error(f"Error occurred while sending the email: {e}")
|
|
278
|
+
if "response" in dir() and getattr(response, "text", None):
|
|
279
|
+
self.logger.error(response.text)
|
|
280
|
+
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cit-pydata
|
|
3
|
-
Version: 0.0.2.
|
|
3
|
+
Version: 0.0.2.dev6
|
|
4
4
|
Summary: Python clients for commonly used api services including Salesforce, SQL, Marketo, Box, SFSync*, and JobLog*
|
|
5
5
|
Author-email: Dionis Wang <dwang@eab.com>, Andrew Fitch <afitch@eab.com>, Dagim Desta <ddesta@eab.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/EAB-IT-DEV/cit-pydata
|
|
@@ -18,6 +18,9 @@ Provides-Extra: box
|
|
|
18
18
|
Requires-Dist: boxsdk; extra == "box"
|
|
19
19
|
Provides-Extra: joblog
|
|
20
20
|
Requires-Dist: marketorestpython; extra == "joblog"
|
|
21
|
+
Provides-Extra: mailer
|
|
22
|
+
Requires-Dist: requests; extra == "mailer"
|
|
23
|
+
Requires-Dist: boto3; extra == "mailer"
|
|
21
24
|
Provides-Extra: salesforce
|
|
22
25
|
Requires-Dist: simple_salesforce; extra == "salesforce"
|
|
23
26
|
Requires-Dist: pyforce; extra == "salesforce"
|
|
@@ -70,6 +73,7 @@ Requires-Dist: pandas; extra == "all"
|
|
|
70
73
|
Requires-Dist: psycopg2-binary; extra == "all"
|
|
71
74
|
Requires-Dist: python-dotenv; extra == "all"
|
|
72
75
|
Requires-Dist: boto3; extra == "all"
|
|
76
|
+
Requires-Dist: requests; extra == "all"
|
|
73
77
|
Dynamic: license-file
|
|
74
78
|
|
|
75
79
|
# cit-pydata
|
|
@@ -13,6 +13,8 @@ src/cit_pydata/box/__init__.py
|
|
|
13
13
|
src/cit_pydata/box/api.py
|
|
14
14
|
src/cit_pydata/joblog/__init__.py
|
|
15
15
|
src/cit_pydata/joblog/api.py
|
|
16
|
+
src/cit_pydata/mailer/__init__.py
|
|
17
|
+
src/cit_pydata/mailer/api.py
|
|
16
18
|
src/cit_pydata/marketo/__init__.py
|
|
17
19
|
src/cit_pydata/marketo/api.py
|
|
18
20
|
src/cit_pydata/salesforce/__init__.py
|
|
@@ -14,6 +14,7 @@ numpy
|
|
|
14
14
|
psycopg2-binary
|
|
15
15
|
python-dotenv
|
|
16
16
|
boto3
|
|
17
|
+
requests
|
|
17
18
|
|
|
18
19
|
[aws]
|
|
19
20
|
aws-lambda-powertools
|
|
@@ -28,6 +29,10 @@ marketorestpython
|
|
|
28
29
|
[local]
|
|
29
30
|
python-dotenv
|
|
30
31
|
|
|
32
|
+
[mailer]
|
|
33
|
+
requests
|
|
34
|
+
boto3
|
|
35
|
+
|
|
31
36
|
[salesforce]
|
|
32
37
|
simple_salesforce
|
|
33
38
|
pyforce
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cit_pydata-0.0.2.dev4 → cit_pydata-0.0.2.dev6}/src/cit_pydata.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|