databutton 0.31.0__py3-none-any.whl → 0.31.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.
@@ -1,3 +1,8 @@
1
1
  from .email import email
2
2
 
3
- __all__ = ["email", "create_attachment"]
3
+ __all__ = [
4
+ "email",
5
+ "attachment_from_bytes",
6
+ "attachment_from_str",
7
+ "attachment_from_file",
8
+ ]
@@ -1,9 +1,11 @@
1
1
  import base64
2
+ import io
2
3
  import mimetypes
3
4
  import re
4
5
  from collections.abc import Sequence
5
6
  from typing import List, Optional, Union
6
7
 
8
+ import pandas as pd
7
9
  from pydantic import BaseModel
8
10
 
9
11
  from .send import send
@@ -37,17 +39,17 @@ def validate_email_to_arg(to: Union[str, List[str]]) -> List[str]:
37
39
  class Attachment(BaseModel):
38
40
  """An attachment to be included with an email."""
39
41
 
40
- # Filename
41
- name: Optional[str] = None
42
+ # Attachment file name
43
+ file_name: Optional[str] = None
42
44
 
43
45
  # MIME type of the attachment
44
- type: Optional[str] = None
46
+ content_type: Optional[str] = None
45
47
 
46
- # ID to use for inline attachments
47
- cid: Optional[str] = None
48
+ # Content ID (CID) to use for inline attachments
49
+ content_id: Optional[str] = None
48
50
 
49
51
  # Base64 encoded data
50
- base64_content: str
52
+ content_base64: str
51
53
 
52
54
 
53
55
  # This is the type expected in the api
@@ -76,39 +78,149 @@ def encode_content(content: bytes | str) -> str:
76
78
  return base64.b64encode(content).decode()
77
79
 
78
80
 
79
- def create_attachment(
81
+ def attachment_from_bytes(
82
+ content: bytes,
80
83
  *,
81
- content: bytes | str,
82
- name: Optional[str] = None,
83
- type: Optional[str] = None,
84
+ file_name: Optional[str] = None,
85
+ content_type: Optional[str] = None,
84
86
  cid: Optional[str] = None,
85
87
  ) -> Attachment:
86
- """Create an attachment to be included with an email.
88
+ """Create attachment with content as raw bytes.
87
89
 
88
- Content can either be a string or a bytes object.
89
- We will base64 encode it for you.
90
+ You can optionally provide a file name and/or content type.
90
91
 
91
- The content type can be omitted if the name has a normal file extension.
92
+ If missing we will try to guess the content type from the file name.
92
93
 
93
94
  To use an attachment as an inline image in the email,
94
95
  set the `cid="my_image_id"` parameter,
95
96
  and use `<img src="cid:my_image_id">` in the html content.
96
97
  """
97
98
  return Attachment(
98
- name=name,
99
- type=determine_type(type, name),
100
- base64_content=encode_content(content),
99
+ file_name=file_name,
100
+ content_type=determine_type(content_type, file_name),
101
+ content_base64=encode_content(content),
102
+ content_id=cid,
103
+ )
104
+
105
+
106
+ def attachment_from_str(
107
+ content: str,
108
+ *,
109
+ file_name: Optional[str] = None,
110
+ content_type: Optional[str] = None,
111
+ cid: Optional[str] = None,
112
+ ) -> Attachment:
113
+ """Create attachment with content as raw str."""
114
+ return attachment_from_bytes(
115
+ content.encode(),
116
+ file_name=file_name,
117
+ content_type=content_type,
118
+ cid=cid,
119
+ )
120
+
121
+
122
+ def attachment_from_file(
123
+ fp: Optional[io.IOBase] = None,
124
+ *,
125
+ file_name: Optional[str] = None,
126
+ content_type: Optional[str] = None,
127
+ cid: Optional[str] = None,
128
+ ) -> Attachment:
129
+ """Create attachment with content from a file.
130
+
131
+ fp can be anything with a .read() method returning bytes or str,
132
+ or omitted to read file_name from file system.
133
+ """
134
+ if fp is None:
135
+ if file_name is None:
136
+ raise ValueError("Either `fp` or `file_name` must be provided.")
137
+ with open(file_name, "rb") as fp:
138
+ buf = fp.read()
139
+ else:
140
+ buf = fp.read()
141
+ if isinstance(buf, str):
142
+ buf = buf.encode()
143
+ return attachment_from_bytes(
144
+ buf,
145
+ file_name=file_name,
146
+ content_type=content_type,
147
+ cid=cid,
148
+ )
149
+
150
+
151
+ def attachment_from_pil_image_as_jpeg(
152
+ image, # PIL image
153
+ *,
154
+ image_format: str = "JPEG",
155
+ file_name: Optional[str] = None,
156
+ content_type: Optional[str] = None,
157
+ cid: Optional[str] = None,
158
+ ) -> Attachment:
159
+ """Create image attachment with content from a PIL compatible image (such as pillow).
160
+
161
+ This convenience function calls image.save(buffer, format=image_format),
162
+ to further customize image formats etc take a peek at the implementation
163
+ here and use attachment_from_bytes directly instead.
164
+
165
+ You can optionally provide a file name and/or content type.
166
+
167
+ If missing we will try to guess the content type from the file name.
168
+
169
+ To use an attachment as an inline image in the email,
170
+ set the `cid="my_image_id"` parameter,
171
+ and use `<img src="cid:my_image_id">` in the html content.
172
+ """
173
+ buf = io.BytesIO()
174
+ image.save(buf, format=image_format)
175
+ return attachment_from_bytes(
176
+ buf.getvalue(),
177
+ file_name=file_name,
178
+ content_type=content_type,
101
179
  cid=cid,
102
180
  )
103
181
 
104
182
 
183
+ def attachment_from_dataframe_as_csv(
184
+ df: pd.DataFrame,
185
+ *,
186
+ file_name: Optional[str] = None,
187
+ ) -> Attachment:
188
+ """Create CSV attachment with content from a pandas dataframe."""
189
+ return attachment_from_str(
190
+ df.to_csv(),
191
+ file_name=file_name,
192
+ content_type="text/csv",
193
+ )
194
+
195
+
196
+ MIME_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
197
+
198
+
199
+ def attachment_from_dataframe_as_excel(
200
+ df: pd.DataFrame,
201
+ *,
202
+ file_name: Optional[str] = None,
203
+ ) -> Attachment:
204
+ """Create Excel (.xlsx) attachment with content from a pandas dataframe.
205
+
206
+ Requires the openpyxl package to be installed.
207
+ """
208
+ buf = io.BytesIO()
209
+ df.to_excel(buf)
210
+ return attachment_from_bytes(
211
+ buf.getvalue(),
212
+ file_name=file_name,
213
+ content_type=MIME_TYPE_XLSX,
214
+ )
215
+
216
+
105
217
  def validate_attachment(att: Attachment) -> Attachment:
106
218
  assert isinstance(att, Attachment)
107
- assert att.type
108
- assert att.name
109
- assert att.base64_content
110
- assert isinstance(att.base64_content, str)
111
- assert re.match(r"^[A-Za-z0-9+/=]+$", att.base64_content)
219
+ assert att.content_type
220
+ assert att.file_name
221
+ assert att.content_base64
222
+ assert isinstance(att.content_base64, str)
223
+ assert re.match(r"^[A-Za-z0-9+/=]+$", att.content_base64)
112
224
  return att
113
225
 
114
226
 
@@ -126,7 +238,7 @@ def create_email(
126
238
  size = (
127
239
  len(content_html or "")
128
240
  + len(content_text or "")
129
- + sum([len(att.base64_content) for att in attachments])
241
+ + sum([len(att.content_base64) for att in attachments])
130
242
  )
131
243
  max = 30 * 1024**2 # 30 MB
132
244
  headroom = 100 * 1024 # leave some room for headers etc
databutton/version.py CHANGED
@@ -5,4 +5,4 @@ This module contains project version information.
5
5
  .. moduleauthor:: Databutton <support@databutton.com>
6
6
  """
7
7
 
8
- __version__ = "0.31.0"
8
+ __version__ = "0.31.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: databutton
3
- Version: 0.31.0
3
+ Version: 0.31.1
4
4
  Summary: The CLI for databutton.com
5
5
  License: Databutton
6
6
  Author: Databutton
@@ -10,16 +10,16 @@ databutton/internal/performedby.py,sha256=QZ4XtsZSbltYfmZesfqVW-v7t153_ziIbCwjWD
10
10
  databutton/internal/retries.py,sha256=OMxnreq1Icaf8OrtFPPNEZ8tCZe6HUI9fJ4FQ6Uiw-M,2011
11
11
  databutton/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  databutton/jobs/run.py,sha256=Td0BSiIrYKiymDKVf8w5MT3C5uiwzeulYNJUGcxQk2k,632
13
- databutton/notify/__init__.py,sha256=QO5y-Y-2y6thE84M7S1k1XVDQ1kaln1sHwLkyrAXDsQ,67
14
- databutton/notify/email.py,sha256=hROf3IEZZWl9cJwJefsXSghYGXJ5jyogAv9d_Vw643Y,4762
13
+ databutton/notify/__init__.py,sha256=PWuuSBzsRFd0-gc6PS2oAMl4gSMSrgLAwTX2GiOaGyE,137
14
+ databutton/notify/email.py,sha256=jfnDWtBTw8gosfEM5Oes2jeKkAKjgL3cD1NK_Usy3Cs,7932
15
15
  databutton/notify/send.py,sha256=cj-TG0jwolEXwJJVxeBxwpfg_it03KoImUn30qAbbQE,749
16
16
  databutton/secrets/__init__.py,sha256=sI0okrfRBs2l5tcLTrKc9uUfPVVRMM9D7WED8p226tI,128
17
17
  databutton/secrets/secrets.py,sha256=pehXsD09iKTxHaNQJ6awvIHXgRXBJrf4Xhrc11ZUeOk,3427
18
18
  databutton/storage/__init__.py,sha256=7ZNd4eQQ7lYYIe1MqCuFbFF1aAVCuj2c_zmJfaPVlDY,252
19
19
  databutton/storage/storage.py,sha256=XBy6z005K0KqmiTkq9eju87PgTboV-vPN1RRXuC57KI,17240
20
20
  databutton/user.py,sha256=QWyWV_G_bnfIJ9js2itOwBk4zqXgBUF9h8lf5mygef8,503
21
- databutton/version.py,sha256=Wpp7aD59eRPATKtGt4Ko7GFEKsW7zAMWDJegHbbW2pI,175
22
- databutton-0.31.0.dist-info/LICENSE,sha256=c7h4pcVZapFEmqWoRTRv_S1P_aibrtDobqmlR-QtMUk,45
23
- databutton-0.31.0.dist-info/METADATA,sha256=Cu0vHHn-S-4-pSQzqLG4JXfiHQrC5kIjnN3mTYcbLAQ,2763
24
- databutton-0.31.0.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
25
- databutton-0.31.0.dist-info/RECORD,,
21
+ databutton/version.py,sha256=eK4hE95pRdHCVWV4DPs-Hciq-3mDMMnVk8WH6hG2IwI,175
22
+ databutton-0.31.1.dist-info/LICENSE,sha256=c7h4pcVZapFEmqWoRTRv_S1P_aibrtDobqmlR-QtMUk,45
23
+ databutton-0.31.1.dist-info/METADATA,sha256=rlHUOLZkCxKTne3HNFxzauvKauuE_aqQftIVFiQm-Xw,2763
24
+ databutton-0.31.1.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
25
+ databutton-0.31.1.dist-info/RECORD,,