ccflow-email 0.2.1__tar.gz → 0.2.2__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.
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/.gitignore +31 -28
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/PKG-INFO +9 -8
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/ccflow_email/__init__.py +1 -1
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/ccflow_email/model.py +21 -19
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/ccflow_email/tests/test_model.py +26 -0
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/pyproject.toml +45 -26
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/LICENSE +0 -0
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/README.md +0 -0
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/ccflow_email/publisher.py +0 -0
- {ccflow_email-0.2.1 → ccflow_email-0.2.2}/ccflow_email/tests/conftest.py +0 -0
|
@@ -5,12 +5,22 @@ __pycache__/
|
|
|
5
5
|
|
|
6
6
|
# C extensions
|
|
7
7
|
*.a
|
|
8
|
+
*.o
|
|
9
|
+
*.dSYM
|
|
8
10
|
*.so
|
|
9
11
|
*.obj
|
|
10
12
|
*.dll
|
|
11
13
|
*.exp
|
|
12
14
|
*.lib
|
|
13
15
|
|
|
16
|
+
# vcpkg
|
|
17
|
+
vcpkg/
|
|
18
|
+
vcpkg_installed/
|
|
19
|
+
|
|
20
|
+
# Rust
|
|
21
|
+
target
|
|
22
|
+
target-capi
|
|
23
|
+
|
|
14
24
|
# Distribution / packaging
|
|
15
25
|
.Python
|
|
16
26
|
build/
|
|
@@ -55,26 +65,12 @@ junit.xml
|
|
|
55
65
|
.hypothesis/
|
|
56
66
|
.pytest_cache/
|
|
57
67
|
|
|
58
|
-
#
|
|
59
|
-
*.mo
|
|
60
|
-
*.pot
|
|
61
|
-
|
|
62
|
-
# Django stuff:
|
|
68
|
+
# Django
|
|
63
69
|
*.log
|
|
64
70
|
local_settings.py
|
|
65
71
|
db.sqlite3
|
|
66
72
|
db.sqlite3-journal
|
|
67
73
|
|
|
68
|
-
# Flask stuff:
|
|
69
|
-
instance/
|
|
70
|
-
.webassets-cache
|
|
71
|
-
|
|
72
|
-
# Scrapy stuff:
|
|
73
|
-
.scrapy
|
|
74
|
-
|
|
75
|
-
# PyBuilder
|
|
76
|
-
target/
|
|
77
|
-
|
|
78
74
|
# IPython
|
|
79
75
|
profile_default/
|
|
80
76
|
ipython_config.py
|
|
@@ -85,15 +81,12 @@ ipython_config.py
|
|
|
85
81
|
# pipenv
|
|
86
82
|
Pipfile.lock
|
|
87
83
|
|
|
88
|
-
#
|
|
89
|
-
__pypackages__/
|
|
90
|
-
|
|
91
|
-
# Celery stuff
|
|
84
|
+
# Celery
|
|
92
85
|
celerybeat-schedule
|
|
93
86
|
celerybeat.pid
|
|
94
87
|
|
|
95
|
-
#
|
|
96
|
-
|
|
88
|
+
# Airspeed Velocity
|
|
89
|
+
.asv
|
|
97
90
|
|
|
98
91
|
# Environments
|
|
99
92
|
.env
|
|
@@ -126,11 +119,12 @@ dmypy.json
|
|
|
126
119
|
/site
|
|
127
120
|
index.md
|
|
128
121
|
docs/_build/
|
|
129
|
-
docs/src/_build/
|
|
130
122
|
docs/api
|
|
131
|
-
docs/index.md
|
|
132
123
|
docs/html
|
|
124
|
+
docs/index.md
|
|
133
125
|
docs/jupyter_execute
|
|
126
|
+
docs/src/_build/
|
|
127
|
+
docs/superpowers
|
|
134
128
|
index.md
|
|
135
129
|
|
|
136
130
|
# JS
|
|
@@ -141,19 +135,28 @@ js/node_modules
|
|
|
141
135
|
js/test-results
|
|
142
136
|
js/playwright-report
|
|
143
137
|
js/*.tgz
|
|
144
|
-
ccflow_email/extension
|
|
145
138
|
|
|
146
139
|
# Jupyter
|
|
147
140
|
.ipynb_checkpoints
|
|
148
141
|
.autoversion
|
|
149
142
|
Untitled*.ipynb
|
|
150
|
-
|
|
151
|
-
!ccflow_email/extension/install.json
|
|
143
|
+
ccflow_email/extension
|
|
152
144
|
ccflow_email/nbextension
|
|
153
145
|
ccflow_email/labextension
|
|
154
146
|
|
|
147
|
+
# Emscripten SDK (locally installed)
|
|
148
|
+
emsdk
|
|
149
|
+
.pyodide_build/
|
|
150
|
+
|
|
155
151
|
# Mac
|
|
156
152
|
.DS_Store
|
|
157
153
|
|
|
158
|
-
#
|
|
159
|
-
|
|
154
|
+
# Hydra
|
|
155
|
+
/outputs/
|
|
156
|
+
/multirun/
|
|
157
|
+
|
|
158
|
+
# AI
|
|
159
|
+
ROADMAP.md
|
|
160
|
+
AGENTS.md
|
|
161
|
+
.github/hooks/sdlc.json
|
|
162
|
+
.superpowers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: ccflow-email
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: ccflow models for email
|
|
5
5
|
Project-URL: Repository, https://github.com/1kbgz/ccflow-email
|
|
6
6
|
Project-URL: Homepage, https://github.com/1kbgz/ccflow-email
|
|
@@ -10,28 +10,29 @@ License-File: LICENSE
|
|
|
10
10
|
Classifier: Development Status :: 3 - Alpha
|
|
11
11
|
Classifier: Programming Language :: Python
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
17
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.11
|
|
21
20
|
Requires-Dist: ccflow
|
|
22
21
|
Requires-Dist: emails
|
|
23
22
|
Provides-Extra: develop
|
|
24
23
|
Requires-Dist: build; extra == 'develop'
|
|
25
24
|
Requires-Dist: bump-my-version; extra == 'develop'
|
|
26
|
-
Requires-Dist: check-
|
|
27
|
-
Requires-Dist: codespell
|
|
25
|
+
Requires-Dist: check-dist; extra == 'develop'
|
|
26
|
+
Requires-Dist: codespell; extra == 'develop'
|
|
28
27
|
Requires-Dist: hatchling; extra == 'develop'
|
|
28
|
+
Requires-Dist: mdformat; extra == 'develop'
|
|
29
|
+
Requires-Dist: mdformat-myst; extra == 'develop'
|
|
29
30
|
Requires-Dist: mdformat-tables>=1; extra == 'develop'
|
|
30
|
-
Requires-Dist: mdformat<1.1,>=0.7.22; extra == 'develop'
|
|
31
31
|
Requires-Dist: pytest; extra == 'develop'
|
|
32
32
|
Requires-Dist: pytest-cov; extra == 'develop'
|
|
33
|
-
Requires-Dist: ruff
|
|
33
|
+
Requires-Dist: ruff; extra == 'develop'
|
|
34
34
|
Requires-Dist: twine; extra == 'develop'
|
|
35
|
+
Requires-Dist: ty; extra == 'develop'
|
|
35
36
|
Requires-Dist: uv; extra == 'develop'
|
|
36
37
|
Requires-Dist: wheel; extra == 'develop'
|
|
37
38
|
Description-Content-Type: text/markdown
|
|
@@ -1,25 +1,24 @@
|
|
|
1
|
-
from typing import List, Optional, Tuple, Union
|
|
2
|
-
|
|
3
1
|
from ccflow import BaseModel
|
|
4
2
|
from pydantic import Field, field_validator, model_validator
|
|
5
3
|
|
|
6
4
|
__all__ = (
|
|
7
|
-
"Message",
|
|
8
5
|
"SMTP",
|
|
9
6
|
"Attachment",
|
|
10
7
|
"Email",
|
|
8
|
+
"Message",
|
|
11
9
|
)
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
class Message(BaseModel):
|
|
15
|
-
content:
|
|
16
|
-
subject:
|
|
17
|
-
from_:
|
|
18
|
-
to_:
|
|
19
|
-
cc:
|
|
20
|
-
bcc:
|
|
13
|
+
content: str | None = Field(default=None, description="HTML content of the email")
|
|
14
|
+
subject: str | None = Field(default=None, description="Subject of the email")
|
|
15
|
+
from_: tuple[str, str] | str | None = Field(default=None, description="Sender email address")
|
|
16
|
+
to_: tuple[str, str] | str | None = Field(default=None, description="Recipient email address")
|
|
17
|
+
cc: tuple[str, str] | str | None = Field(default=None, description="CC email address")
|
|
18
|
+
bcc: tuple[str, str] | str | None = Field(default=None, description="BCC email address")
|
|
21
19
|
|
|
22
20
|
@field_validator("from_")
|
|
21
|
+
@classmethod
|
|
23
22
|
def _validate_from(cls, v):
|
|
24
23
|
# If tuple, must be (name, email)
|
|
25
24
|
if isinstance(v, tuple):
|
|
@@ -30,6 +29,7 @@ class Message(BaseModel):
|
|
|
30
29
|
return v
|
|
31
30
|
|
|
32
31
|
@field_validator("to_")
|
|
32
|
+
@classmethod
|
|
33
33
|
def _validate_to(cls, v):
|
|
34
34
|
# If tuple, must be (name, email)
|
|
35
35
|
if isinstance(v, tuple):
|
|
@@ -40,6 +40,7 @@ class Message(BaseModel):
|
|
|
40
40
|
return v
|
|
41
41
|
|
|
42
42
|
@field_validator("cc")
|
|
43
|
+
@classmethod
|
|
43
44
|
def _validate_cc(cls, v):
|
|
44
45
|
# If tuple, must be (name, email)
|
|
45
46
|
if isinstance(v, tuple):
|
|
@@ -50,6 +51,7 @@ class Message(BaseModel):
|
|
|
50
51
|
return v
|
|
51
52
|
|
|
52
53
|
@field_validator("bcc")
|
|
54
|
+
@classmethod
|
|
53
55
|
def _validate_bcc(cls, v):
|
|
54
56
|
# If tuple, must be (name, email)
|
|
55
57
|
if isinstance(v, tuple):
|
|
@@ -62,12 +64,12 @@ class Message(BaseModel):
|
|
|
62
64
|
|
|
63
65
|
class SMTP(BaseModel):
|
|
64
66
|
host: str = Field(..., description="SMTP server host")
|
|
65
|
-
port:
|
|
66
|
-
user:
|
|
67
|
-
password:
|
|
68
|
-
tls:
|
|
69
|
-
ssl:
|
|
70
|
-
timeout:
|
|
67
|
+
port: int | None = Field(default=25, description="SMTP server port")
|
|
68
|
+
user: str | None = Field(default=None, description="SMTP server username")
|
|
69
|
+
password: str | None = Field(default=None, repr=False, description="SMTP server password")
|
|
70
|
+
tls: bool | None = Field(default=False, description="Use TLS for SMTP connection")
|
|
71
|
+
ssl: bool | None = Field(default=False, description="Use SSL for SMTP connection")
|
|
72
|
+
timeout: int | None = Field(default=30, description="Timeout for SMTP connection in seconds")
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
class Attachment(BaseModel):
|
|
@@ -79,7 +81,7 @@ class Attachment(BaseModel):
|
|
|
79
81
|
class Email(BaseModel):
|
|
80
82
|
message: Message = Field(description="Email message details")
|
|
81
83
|
smtp: SMTP = Field(description="SMTP server configuration")
|
|
82
|
-
attachments:
|
|
84
|
+
attachments: list[Attachment] | None = Field(default_factory=list, description="List of email attachments")
|
|
83
85
|
|
|
84
86
|
@model_validator(mode="after")
|
|
85
87
|
def _validate_from(self):
|
|
@@ -91,7 +93,7 @@ class Email(BaseModel):
|
|
|
91
93
|
self.smtp.user = self.message.from_[1] if isinstance(self.message.from_, tuple) else self.message.from_
|
|
92
94
|
return self
|
|
93
95
|
|
|
94
|
-
def send(self, to:
|
|
96
|
+
def send(self, to: str | tuple[str | None, str] | list[str | tuple[str | None, str]] | None = None, render: dict | None = None):
|
|
95
97
|
# NOTE: defer import
|
|
96
98
|
from emails import Message as EmailMessage
|
|
97
99
|
|
|
@@ -111,10 +113,10 @@ class Email(BaseModel):
|
|
|
111
113
|
bcc=self.message.bcc,
|
|
112
114
|
)
|
|
113
115
|
|
|
114
|
-
for attachment in self.attachments:
|
|
116
|
+
for attachment in self.attachments or ():
|
|
115
117
|
msg.attach(filename=attachment.filename, content_disposition=attachment.content_disposition, data=attachment.data)
|
|
116
118
|
|
|
117
|
-
smtp_config = self.smtp.model_dump(exclude_unset=True, exclude_none=True, exclude=
|
|
119
|
+
smtp_config = self.smtp.model_dump(exclude_unset=True, exclude_none=True, exclude={"type_"})
|
|
118
120
|
smtp_config["fail_silently"] = False
|
|
119
121
|
response = msg.send(to=to, render=render or {}, smtp=smtp_config)
|
|
120
122
|
return response
|
|
@@ -15,6 +15,7 @@ class TestEmail:
|
|
|
15
15
|
)
|
|
16
16
|
assert email.message.subject == "Test Email"
|
|
17
17
|
assert email.smtp.host == "smtp.example.com"
|
|
18
|
+
assert email.attachments is not None
|
|
18
19
|
assert email.attachments[0].filename == "test.txt"
|
|
19
20
|
|
|
20
21
|
@pytest.mark.skipif(not environ.get("SMTP_HOST"), reason="SMTP server not configured")
|
|
@@ -73,3 +74,28 @@ class TestEmail:
|
|
|
73
74
|
|
|
74
75
|
assert response.success is True
|
|
75
76
|
assert response.status_text == b"Message received"
|
|
77
|
+
|
|
78
|
+
def test_send_without_attachments(self):
|
|
79
|
+
email = Email(
|
|
80
|
+
message=Message(content="Hello", subject="Test", from_="sender@example.com"),
|
|
81
|
+
smtp=SMTP(host="smtp.example.com"),
|
|
82
|
+
attachments=None,
|
|
83
|
+
)
|
|
84
|
+
recipient = ("Recipient", "recipient@example.com")
|
|
85
|
+
with patch("emails.Message") as message:
|
|
86
|
+
response = email.send(to=recipient)
|
|
87
|
+
message.assert_called_once_with(html="Hello", subject="Test", mail_from="sender@example.com", mail_to=recipient, cc=None, bcc=None)
|
|
88
|
+
message.return_value.attach.assert_not_called()
|
|
89
|
+
assert response is message.return_value.send.return_value
|
|
90
|
+
assert "type_" not in message.return_value.send.call_args.kwargs["smtp"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TestSMTP:
|
|
94
|
+
def test_repr_omits_password(self):
|
|
95
|
+
smtp = SMTP(host="smtp.example.com", user="user", password="smtp-secret")
|
|
96
|
+
email = Email(message=Message(content="Hi", subject="Test", from_="user"), smtp=smtp)
|
|
97
|
+
|
|
98
|
+
for value in (smtp, email):
|
|
99
|
+
assert "smtp-secret" not in repr(value)
|
|
100
|
+
assert "smtp-secret" not in str(value)
|
|
101
|
+
assert "smtp.example.com" in repr(smtp)
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = [
|
|
3
|
-
|
|
2
|
+
requires = [
|
|
3
|
+
"hatchling",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "hatchling.build"
|
|
4
6
|
|
|
5
7
|
[project]
|
|
6
8
|
name = "ccflow-email"
|
|
7
|
-
authors = [
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "1kbgz", email = "dev@1kbgz.com"},
|
|
11
|
+
]
|
|
8
12
|
description = "ccflow models for email"
|
|
9
13
|
readme = "README.md"
|
|
10
14
|
license = { text = "Apache-2.0" }
|
|
11
|
-
version = "0.2.
|
|
12
|
-
requires-python = ">=3.
|
|
15
|
+
version = "0.2.2"
|
|
16
|
+
requires-python = ">=3.11"
|
|
13
17
|
keywords = []
|
|
14
18
|
|
|
15
19
|
classifiers = [
|
|
@@ -18,7 +22,6 @@ classifiers = [
|
|
|
18
22
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
19
23
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
20
24
|
"Programming Language :: Python :: 3",
|
|
21
|
-
"Programming Language :: Python :: 3.10",
|
|
22
25
|
"Programming Language :: Python :: 3.11",
|
|
23
26
|
"Programming Language :: Python :: 3.12",
|
|
24
27
|
"Programming Language :: Python :: 3.13",
|
|
@@ -34,15 +37,17 @@ dependencies = [
|
|
|
34
37
|
develop = [
|
|
35
38
|
"build",
|
|
36
39
|
"bump-my-version",
|
|
37
|
-
"check-
|
|
38
|
-
"codespell
|
|
40
|
+
"check-dist",
|
|
41
|
+
"codespell",
|
|
39
42
|
"hatchling",
|
|
40
|
-
"mdformat
|
|
43
|
+
"mdformat",
|
|
44
|
+
"mdformat-myst",
|
|
41
45
|
"mdformat-tables>=1",
|
|
42
46
|
"pytest",
|
|
43
47
|
"pytest-cov",
|
|
44
|
-
"ruff
|
|
48
|
+
"ruff",
|
|
45
49
|
"twine",
|
|
50
|
+
"ty",
|
|
46
51
|
"uv",
|
|
47
52
|
"wheel",
|
|
48
53
|
]
|
|
@@ -54,7 +59,7 @@ Repository = "https://github.com/1kbgz/ccflow-email"
|
|
|
54
59
|
Homepage = "https://github.com/1kbgz/ccflow-email"
|
|
55
60
|
|
|
56
61
|
[tool.bumpversion]
|
|
57
|
-
current_version = "0.2.
|
|
62
|
+
current_version = "0.2.2"
|
|
58
63
|
commit = true
|
|
59
64
|
tag = true
|
|
60
65
|
commit_args = "-s"
|
|
@@ -69,18 +74,12 @@ filename = "pyproject.toml"
|
|
|
69
74
|
search = 'version = "{current_version}"'
|
|
70
75
|
replace = 'version = "{new_version}"'
|
|
71
76
|
|
|
72
|
-
[tool.check-manifest]
|
|
73
|
-
ignore = [
|
|
74
|
-
".copier-answers.yaml",
|
|
75
|
-
"Makefile",
|
|
76
|
-
"docs/**/*",
|
|
77
|
-
]
|
|
78
|
-
|
|
79
77
|
[tool.coverage.run]
|
|
80
78
|
branch = true
|
|
81
79
|
omit = [
|
|
82
|
-
"ccflow_email/tests/integration
|
|
80
|
+
"ccflow_email/tests/integration/*",
|
|
83
81
|
]
|
|
82
|
+
|
|
84
83
|
[tool.coverage.report]
|
|
85
84
|
exclude_also = [
|
|
86
85
|
"raise NotImplementedError",
|
|
@@ -97,28 +96,48 @@ artifacts = []
|
|
|
97
96
|
src = "/"
|
|
98
97
|
|
|
99
98
|
[tool.hatch.build.targets.sdist]
|
|
100
|
-
packages = [
|
|
99
|
+
packages = [
|
|
100
|
+
"ccflow_email",
|
|
101
|
+
]
|
|
101
102
|
|
|
102
103
|
[tool.hatch.build.targets.wheel]
|
|
103
|
-
packages = [
|
|
104
|
+
packages = [
|
|
105
|
+
"ccflow_email",
|
|
106
|
+
]
|
|
104
107
|
|
|
105
108
|
[tool.hatch.build.targets.wheel.shared-data]
|
|
106
109
|
|
|
107
110
|
[tool.pytest.ini_options]
|
|
108
|
-
addopts = [
|
|
111
|
+
addopts = [
|
|
112
|
+
"-vvv",
|
|
113
|
+
"--junitxml=junit.xml",
|
|
114
|
+
]
|
|
109
115
|
testpaths = "ccflow_email/tests"
|
|
110
116
|
|
|
111
117
|
[tool.ruff]
|
|
112
118
|
line-length = 150
|
|
113
119
|
|
|
114
120
|
[tool.ruff.lint]
|
|
115
|
-
extend-select = [
|
|
121
|
+
extend-select = [
|
|
122
|
+
"I",
|
|
123
|
+
]
|
|
116
124
|
|
|
117
125
|
[tool.ruff.lint.isort]
|
|
118
126
|
combine-as-imports = true
|
|
119
127
|
default-section = "third-party"
|
|
120
|
-
known-first-party = [
|
|
121
|
-
|
|
128
|
+
known-first-party = [
|
|
129
|
+
"ccflow_email",
|
|
130
|
+
]
|
|
131
|
+
section-order = [
|
|
132
|
+
"future",
|
|
133
|
+
"standard-library",
|
|
134
|
+
"third-party",
|
|
135
|
+
"first-party",
|
|
136
|
+
"local-folder",
|
|
137
|
+
]
|
|
122
138
|
|
|
123
139
|
[tool.ruff.lint.per-file-ignores]
|
|
124
|
-
"__init__.py" = [
|
|
140
|
+
"__init__.py" = [
|
|
141
|
+
"F401",
|
|
142
|
+
"F403",
|
|
143
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|