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.
@@ -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
- # Translations
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
- # PEP 582; used by e.g. github.com/David-OConnor/pyflow
89
- __pypackages__/
90
-
91
- # Celery stuff
84
+ # Celery
92
85
  celerybeat-schedule
93
86
  celerybeat.pid
94
87
 
95
- # SageMath parsed files
96
- *.sage.py
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
- !ccflow_email/extension/ccflow_email.json
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
- # Rust
159
- target
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: ccflow-email
3
- Version: 0.2.1
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.10
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-manifest; extra == 'develop'
27
- Requires-Dist: codespell<2.5,>=2.4; extra == 'develop'
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<0.15,>=0.9; extra == 'develop'
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,4 +1,4 @@
1
1
  from .model import *
2
2
  from .publisher import *
3
3
 
4
- __version__ = "0.2.1"
4
+ __version__ = "0.2.2"
@@ -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: Optional[str] = Field(default=None, description="HTML content of the email")
16
- subject: Optional[str] = Field(default=None, description="Subject of the email")
17
- from_: Optional[Union[Tuple[str, str], str]] = Field(default=None, description="Sender email address")
18
- to_: Optional[Union[Tuple[str, str], str]] = Field(default=None, description="Recipient email address")
19
- cc: Optional[Union[Tuple[str, str], str]] = Field(default=None, description="CC email address")
20
- bcc: Optional[Union[Tuple[str, str], str]] = Field(default=None, description="BCC email address")
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: Optional[int] = Field(default=25, description="SMTP server port")
66
- user: Optional[str] = Field(default=None, description="SMTP server username")
67
- password: Optional[str] = Field(default=None, description="SMTP server password")
68
- tls: Optional[bool] = Field(default=False, description="Use TLS for SMTP connection")
69
- ssl: Optional[bool] = Field(default=False, description="Use SSL for SMTP connection")
70
- timeout: Optional[int] = Field(default=30, description="Timeout for SMTP connection in seconds")
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: Optional[List[Attachment]] = Field(default_factory=list, description="List of email 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: Union[str, list[str]] = None, render: dict = None):
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=["type_"])
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 = ["hatchling"]
3
- build-backend="hatchling.build"
2
+ requires = [
3
+ "hatchling",
4
+ ]
5
+ build-backend = "hatchling.build"
4
6
 
5
7
  [project]
6
8
  name = "ccflow-email"
7
- authors = [{name = "1kbgz", email = "dev@1kbgz.com"}]
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.1"
12
- requires-python = ">=3.10"
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-manifest",
38
- "codespell>=2.4,<2.5",
40
+ "check-dist",
41
+ "codespell",
39
42
  "hatchling",
40
- "mdformat>=0.7.22,<1.1",
43
+ "mdformat",
44
+ "mdformat-myst",
41
45
  "mdformat-tables>=1",
42
46
  "pytest",
43
47
  "pytest-cov",
44
- "ruff>=0.9,<0.15",
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.1"
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 = ["ccflow_email"]
99
+ packages = [
100
+ "ccflow_email",
101
+ ]
101
102
 
102
103
  [tool.hatch.build.targets.wheel]
103
- packages = ["ccflow_email"]
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 = ["-vvv", "--junitxml=junit.xml"]
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 = ["I"]
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 = ["ccflow_email"]
121
- section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
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" = ["F401", "F403"]
140
+ "__init__.py" = [
141
+ "F401",
142
+ "F403",
143
+ ]
File without changes
File without changes