plone.api 2.5.2__tar.gz → 3.0.0__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.
Files changed (66) hide show
  1. {plone_api-2.5.2 → plone_api-3.0.0}/CHANGES.md +55 -0
  2. {plone_api-2.5.2 → plone_api-3.0.0}/PKG-INFO +64 -7
  3. {plone_api-2.5.2 → plone_api-3.0.0}/docs/portal.md +103 -0
  4. {plone_api-2.5.2 → plone_api-3.0.0}/pyproject.toml +12 -11
  5. {plone_api-2.5.2 → plone_api-3.0.0}/setup.py +7 -11
  6. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/addon.py +18 -21
  7. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/content.py +0 -1
  8. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/env.py +0 -1
  9. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/portal.py +0 -1
  10. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/relation.py +0 -1
  11. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/doctests/portal.md +103 -0
  12. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/test_addon.py +0 -1
  13. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/test_content.py +6 -0
  14. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/test_doctests.py +0 -1
  15. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/test_portal.py +0 -1
  16. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/test_user.py +3 -0
  17. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/user.py +24 -4
  18. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone.api.egg-info/PKG-INFO +64 -7
  19. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone.api.egg-info/SOURCES.txt +0 -2
  20. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone.api.egg-info/requires.txt +1 -1
  21. {plone_api-2.5.2 → plone_api-3.0.0}/tox.ini +85 -108
  22. plone_api-2.5.2/src/plone/__init__.py +0 -1
  23. plone_api-2.5.2/src/plone.api.egg-info/namespace_packages.txt +0 -1
  24. {plone_api-2.5.2 → plone_api-3.0.0}/CONTRIBUTING.md +0 -0
  25. {plone_api-2.5.2 → plone_api-3.0.0}/LICENSE +0 -0
  26. {plone_api-2.5.2 → plone_api-3.0.0}/MANIFEST.in +0 -0
  27. {plone_api-2.5.2 → plone_api-3.0.0}/README.md +0 -0
  28. {plone_api-2.5.2 → plone_api-3.0.0}/docs/about.md +0 -0
  29. {plone_api-2.5.2 → plone_api-3.0.0}/docs/addon.md +0 -0
  30. {plone_api-2.5.2 → plone_api-3.0.0}/docs/content.md +0 -0
  31. {plone_api-2.5.2 → plone_api-3.0.0}/docs/contribute.md +0 -0
  32. {plone_api-2.5.2 → plone_api-3.0.0}/docs/env.md +0 -0
  33. {plone_api-2.5.2 → plone_api-3.0.0}/docs/group.md +0 -0
  34. {plone_api-2.5.2 → plone_api-3.0.0}/docs/index.md +0 -0
  35. {plone_api-2.5.2 → plone_api-3.0.0}/docs/relation.md +0 -0
  36. {plone_api-2.5.2 → plone_api-3.0.0}/docs/user.md +0 -0
  37. {plone_api-2.5.2 → plone_api-3.0.0}/setup.cfg +0 -0
  38. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/__init__.py +0 -0
  39. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/configure.zcml +0 -0
  40. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/exc.py +0 -0
  41. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/group.py +0 -0
  42. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/profiles/testfixture/metadata.xml +0 -0
  43. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/profiles/testfixture/types/Dexterity_Folder.xml +0 -0
  44. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/profiles/testfixture/types/Dexterity_Item.xml +0 -0
  45. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/profiles/testfixture/types.xml +0 -0
  46. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/testing.zcml +0 -0
  47. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/Dexterity_Folder.xml +0 -0
  48. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/Dexterity_Item.xml +0 -0
  49. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/__init__.py +0 -0
  50. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/base.py +0 -0
  51. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/doctests/about.md +0 -0
  52. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/doctests/addon.md +0 -0
  53. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/doctests/content.md +0 -0
  54. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/doctests/contribute.md +0 -0
  55. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/doctests/env.md +0 -0
  56. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/doctests/group.md +0 -0
  57. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/doctests/relation.md +0 -0
  58. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/doctests/user.md +0 -0
  59. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/test_env.py +0 -0
  60. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/test_group.py +0 -0
  61. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/test_relation.py +0 -0
  62. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/tests/test_validation.py +0 -0
  63. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone/api/validation.py +0 -0
  64. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone.api.egg-info/dependency_links.txt +0 -0
  65. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone.api.egg-info/not-zip-safe +0 -0
  66. {plone_api-2.5.2 → plone_api-3.0.0}/src/plone.api.egg-info/top_level.txt +0 -0
@@ -9,6 +9,61 @@
9
9
 
10
10
  <!-- towncrier release notes start -->
11
11
 
12
+ ## 3.0.0 (2026-05-07)
13
+
14
+
15
+ ### Documentation
16
+
17
+ - Added example to construct a complex email. @1letter
18
+
19
+ ## 3.0.0a3 (2026-03-25)
20
+
21
+
22
+ ### Bug fixes
23
+
24
+ - Fix ``plone.api.user.create()`` to respect ``use_uuid_as_userid`` and
25
+ ``use_email_as_login`` registry settings, as well as custom ``IUserIdGenerator``
26
+ and ``ILoginNameGenerator`` utilities. #4292
27
+
28
+
29
+ ### Internal
30
+
31
+ - Update configuration files @plone
32
+
33
+
34
+ ### Tests
35
+
36
+ - Fix `test_create_raises_unicodedecodeerror` for deferred indexing queue processing since Products.CMFCore 3.9.
37
+ @jensens #602
38
+
39
+ ## 3.0.0a2 (2025-12-18)
40
+
41
+
42
+ ### Bug fixes:
43
+
44
+ - Fix deprecation warnings for `INonInstallable` and `get_installer`. @mauritsvanrees
45
+
46
+
47
+ ### Internal:
48
+
49
+ - Improve trigger for RTD PR preview builds. @stevepiercy #591
50
+ - Ignore the `/_build` directory from building documentation. @stevepiercy #598
51
+
52
+ ## 3.0.0a1 (2025-11-26)
53
+
54
+
55
+ ### Breaking changes:
56
+
57
+ - Replace ``pkg_resources`` namespace with PEP 420 native namespace.
58
+ Support only Plone 6.2 and Python 3.10+. #3928
59
+
60
+ ## 2.5.3 (2025-09-10)
61
+
62
+
63
+ ### Bug fixes:
64
+
65
+ - Drop `pkg_resources` usage @gforcada #4126
66
+
12
67
  ## 2.5.2 (2025-06-05)
13
68
 
14
69
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: plone.api
3
- Version: 2.5.2
3
+ Version: 3.0.0
4
4
  Summary: A Plone API.
5
5
  Home-page: https://github.com/plone/plone.api
6
6
  Author: Plone Foundation
@@ -19,17 +19,18 @@ Platform: Any
19
19
  Classifier: Development Status :: 5 - Production/Stable
20
20
  Classifier: Environment :: Web Environment
21
21
  Classifier: Framework :: Plone
22
- Classifier: Framework :: Plone :: 6.0
22
+ Classifier: Framework :: Plone :: 6.2
23
23
  Classifier: Framework :: Plone :: Core
24
24
  Classifier: Intended Audience :: Developers
25
25
  Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
26
26
  Classifier: Operating System :: OS Independent
27
27
  Classifier: Programming Language :: Python
28
- Classifier: Programming Language :: Python :: 3.8
29
- Classifier: Programming Language :: Python :: 3.9
30
28
  Classifier: Programming Language :: Python :: 3.10
31
29
  Classifier: Programming Language :: Python :: 3.11
32
- Requires-Python: >=3.8
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Classifier: Programming Language :: Python :: 3.14
33
+ Requires-Python: >=3.10
33
34
  Description-Content-Type: text/markdown
34
35
  License-File: LICENSE
35
36
  Requires-Dist: Acquisition
@@ -37,6 +38,7 @@ Requires-Dist: Products.statusmessages
37
38
  Requires-Dist: Products.PlonePAS
38
39
  Requires-Dist: Products.CMFPlone
39
40
  Requires-Dist: decorator
41
+ Requires-Dist: plone.app.users
40
42
  Requires-Dist: plone.app.uuid
41
43
  Requires-Dist: plone.app.dexterity
42
44
  Requires-Dist: plone.app.intid
@@ -46,7 +48,6 @@ Requires-Dist: plone.dexterity
46
48
  Requires-Dist: plone.i18n
47
49
  Requires-Dist: plone.registry
48
50
  Requires-Dist: plone.uuid
49
- Requires-Dist: setuptools
50
51
  Requires-Dist: zope.globalrequest
51
52
  Requires-Dist: Products.CMFCore
52
53
  Requires-Dist: z3c.relationfield
@@ -71,6 +72,7 @@ Dynamic: description-content-type
71
72
  Dynamic: home-page
72
73
  Dynamic: keywords
73
74
  Dynamic: license
75
+ Dynamic: license-file
74
76
  Dynamic: platform
75
77
  Dynamic: project-url
76
78
  Dynamic: provides-extra
@@ -132,6 +134,61 @@ Continuous Integration
132
134
 
133
135
  <!-- towncrier release notes start -->
134
136
 
137
+ ## 3.0.0 (2026-05-07)
138
+
139
+
140
+ ### Documentation
141
+
142
+ - Added example to construct a complex email. @1letter
143
+
144
+ ## 3.0.0a3 (2026-03-25)
145
+
146
+
147
+ ### Bug fixes
148
+
149
+ - Fix ``plone.api.user.create()`` to respect ``use_uuid_as_userid`` and
150
+ ``use_email_as_login`` registry settings, as well as custom ``IUserIdGenerator``
151
+ and ``ILoginNameGenerator`` utilities. #4292
152
+
153
+
154
+ ### Internal
155
+
156
+ - Update configuration files @plone
157
+
158
+
159
+ ### Tests
160
+
161
+ - Fix `test_create_raises_unicodedecodeerror` for deferred indexing queue processing since Products.CMFCore 3.9.
162
+ @jensens #602
163
+
164
+ ## 3.0.0a2 (2025-12-18)
165
+
166
+
167
+ ### Bug fixes:
168
+
169
+ - Fix deprecation warnings for `INonInstallable` and `get_installer`. @mauritsvanrees
170
+
171
+
172
+ ### Internal:
173
+
174
+ - Improve trigger for RTD PR preview builds. @stevepiercy #591
175
+ - Ignore the `/_build` directory from building documentation. @stevepiercy #598
176
+
177
+ ## 3.0.0a1 (2025-11-26)
178
+
179
+
180
+ ### Breaking changes:
181
+
182
+ - Replace ``pkg_resources`` namespace with PEP 420 native namespace.
183
+ Support only Plone 6.2 and Python 3.10+. #3928
184
+
185
+ ## 2.5.3 (2025-09-10)
186
+
187
+
188
+ ### Bug fixes:
189
+
190
+ - Drop `pkg_resources` usage @gforcada #4126
191
+
135
192
  ## 2.5.2 (2025-06-05)
136
193
 
137
194
 
@@ -257,6 +257,109 @@ api.portal.send_email(
257
257
  % 'attachment; filename="report.xml',
258
258
  % payloads[1]['Content-Disposition']
259
259
  % )
260
+ % mailhost.messages.clear()
261
+
262
+ The following code is a more complex example that constructs an email with a file attachment, HTML, plain text, and mail headers to control the mail response.
263
+
264
+ ```python
265
+ from email.encoders import encode_base64
266
+ from email.header import Header
267
+ from email.mime.base import MIMEBase
268
+ from email.mime.multipart import MIMEMultipart
269
+ from email.mime.text import MIMEText
270
+ from plone import api
271
+
272
+ # we need a message part to bundle the HTML and Plain Text
273
+ textmsgpart = MIMEMultipart("alternative")
274
+
275
+ # create plain text part of email
276
+ plaintextpart = MIMEText("Fill out your plain text", "plain", "utf-8")
277
+
278
+ # create html text of email
279
+ html = "<html><body><p>fill out your text in HTML</p></body></html>"
280
+ htmlpart = MIMEText(html, "html", "utf-8")
281
+
282
+ # bundle the parts
283
+ textmsgpart.attach(plaintextpart)
284
+ textmsgpart.attach(htmlpart)
285
+
286
+ # handle the file attachment
287
+
288
+ # Create a dummy PDF as NamedBlobFile
289
+ from plone.namedfile.file import NamedBlobFile
290
+
291
+ # Minimal PDF content (a valid but nearly empty PDF)
292
+ pdf_content = b"""%PDF-1.4
293
+ 1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
294
+ 2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj
295
+ 3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]/Contents 4 0 R>>endobj
296
+ 4 0 obj<</Length 44>>stream
297
+ BT /F1 12 Tf 100 700 Td (Hello World) Tj ET
298
+ endstream endobj
299
+ xref
300
+ 0 5
301
+ 0000000000 65535 f
302
+ 0000000009 00000 n
303
+ 0000000056 00000 n
304
+ 0000000115 00000 n
305
+ 0000000214 00000 n
306
+ trailer<</Size 5/Root 1 0 R>>
307
+ startxref
308
+ 315
309
+ %%EOF"""
310
+
311
+ attachment = NamedBlobFile(
312
+ data=pdf_content,
313
+ contentType='application/pdf',
314
+ filename='document.pdf'
315
+ )
316
+
317
+ filepart = MIMEBase("application", "pdf")
318
+ filepart.set_payload(attachment.data)
319
+ encode_base64(filepart)
320
+ filepart.add_header(
321
+ "Content-Disposition",
322
+ "attachment",
323
+ filename=(Header(attachment.filename, "utf-8").encode()),
324
+ )
325
+
326
+ # we need a mixed Multipart Message to bundle the text bundle and the attachment
327
+ msg = MIMEMultipart("mixed")
328
+ msg["From"] = "sender@abc"
329
+ msg["To"] = "recipient@zzz"
330
+ msg["Subject"] = "The Mail Subject"
331
+ msg["Reply-To"] = "community@plone.org"
332
+ msg["Return-Path"] = "error@xxx"
333
+
334
+ # add the text message bundle
335
+ msg.attach(textmsgpart)
336
+
337
+ # add the file attachment
338
+ msg.attach(filepart)
339
+
340
+ # send with plone.api
341
+ api.portal.send_email(
342
+ sender=msg["From"],
343
+ recipient=msg["To"],
344
+ subject=msg["Subject"],
345
+ body=msg.as_string()
346
+ )
347
+ ```
348
+
349
+ % invisible-code-block: python
350
+ %
351
+ % self.assertEqual(len(mailhost.messages), 1)
352
+ %
353
+ % msg = message_from_bytes(mailhost.messages[0])
354
+ % payloads = msg.get_payload()
355
+ % self.assertTrue(len(payloads) == 2)
356
+ % self.assertTrue(msg['Reply-To'] == 'community@plone.org')
357
+ % text_payloads = payloads[0].get_payload()
358
+ % self.assertEqual(len(text_payloads), 2)
359
+ % self.assertIn(
360
+ % 'attachment; filename',
361
+ % payloads[1]['Content-Disposition']
362
+ % )
260
363
  % api.portal.PRINTINGMAILHOST_ENABLED = False
261
364
  % mailhost.reset()
262
365
 
@@ -1,8 +1,8 @@
1
1
  # Generated from:
2
- # https://github.com/plone/meta/tree/main/config/default
2
+ # https://github.com/plone/meta/tree/main/src/plone/meta/default
3
3
  # See the inline comments on how to expand/tweak this configuration file
4
4
  [build-system]
5
- requires = ["setuptools>=68.2,<77"]
5
+ requires = ["setuptools>=68.2,<83", "wheel"]
6
6
 
7
7
  [tool.towncrier]
8
8
  directory = "news/"
@@ -14,27 +14,27 @@ underlines = ["", "", ""]
14
14
 
15
15
  [[tool.towncrier.type]]
16
16
  directory = "breaking"
17
- name = "Breaking changes:"
17
+ name = "Breaking changes"
18
18
  showcontent = true
19
19
 
20
20
  [[tool.towncrier.type]]
21
21
  directory = "feature"
22
- name = "New features:"
22
+ name = "New features"
23
23
  showcontent = true
24
24
 
25
25
  [[tool.towncrier.type]]
26
26
  directory = "bugfix"
27
- name = "Bug fixes:"
27
+ name = "Bug fixes"
28
28
  showcontent = true
29
29
 
30
30
  [[tool.towncrier.type]]
31
31
  directory = "internal"
32
- name = "Internal:"
32
+ name = "Internal"
33
33
  showcontent = true
34
34
 
35
35
  [[tool.towncrier.type]]
36
36
  directory = "documentation"
37
- name = "Documentation:"
37
+ name = "Documentation"
38
38
  showcontent = true
39
39
 
40
40
  [[tool.towncrier.type]]
@@ -62,7 +62,7 @@ profile = "plone"
62
62
  ##
63
63
 
64
64
  [tool.black]
65
- target-version = ["py38"]
65
+ target-version = ["py310"]
66
66
 
67
67
  ##
68
68
  # Add extra configuration options in .meta.toml:
@@ -73,7 +73,7 @@ target-version = ["py38"]
73
73
  ##
74
74
 
75
75
  [tool.codespell]
76
- ignore-words-list = "discreet,manuel"
76
+ ignore-words-list = "discreet,assertin,thet,manuel,checkin"
77
77
  skip = "*.po,"
78
78
  ##
79
79
  # Add extra configuration options in .meta.toml:
@@ -121,6 +121,7 @@ Zope = [
121
121
  'Products.CMFCore', 'Products.CMFDynamicViewFTI',
122
122
  ]
123
123
  python-dateutil = ['dateutil']
124
+ pytest-plone = ['pytest', 'zope.pytestlayer', 'plone.testing', 'plone.app.testing']
124
125
  ignore-packages = ['Products.PrintingMailHost', 'plone.app.iterate',]
125
126
 
126
127
  ##
@@ -142,11 +143,11 @@ ignore = [
142
143
  "dependabot.yml",
143
144
  "mx.ini",
144
145
  "tox.ini",
146
+ # From `.meta.toml`, pyproject.check_manifest_ignores.
145
147
  "*.cfg",
146
148
  ".editorconfig",
147
149
  ".readthedocs.yaml",
148
- "constraints_plone52.txt",
149
- "constraints_plone60.txt",
150
+ "constraints_plone62.txt",
150
151
  "constraints.txt",
151
152
  "fix-converted-myst.py",
152
153
  "Makefile",
@@ -1,9 +1,7 @@
1
1
  from pathlib import Path
2
- from setuptools import find_packages
3
2
  from setuptools import setup
4
3
 
5
-
6
- version = "2.5.2"
4
+ version = "3.0.0"
7
5
 
8
6
  long_description = "\n".join(
9
7
  [Path("README.md").read_text(), Path("CHANGES.md").read_text()]
@@ -18,20 +16,18 @@ setup(
18
16
  author="Plone Foundation",
19
17
  author_email="plone-developers@lists.sourceforge.net",
20
18
  license="GPL version 2",
21
- packages=find_packages("src"),
22
- package_dir={"": "src"},
23
- namespace_packages=["plone"],
24
19
  include_package_data=True,
25
20
  zip_safe=False,
26
21
  url="https://github.com/plone/plone.api",
27
22
  keywords="plone api",
28
- python_requires=">=3.8",
23
+ python_requires=">=3.10",
29
24
  install_requires=[
30
25
  "Acquisition",
31
26
  "Products.statusmessages",
32
27
  "Products.PlonePAS",
33
28
  "Products.CMFPlone",
34
29
  "decorator",
30
+ "plone.app.users",
35
31
  "plone.app.uuid",
36
32
  "plone.app.dexterity",
37
33
  "plone.app.intid",
@@ -41,7 +37,6 @@ setup(
41
37
  "plone.i18n",
42
38
  "plone.registry",
43
39
  "plone.uuid",
44
- "setuptools",
45
40
  "zope.globalrequest",
46
41
  "Products.CMFCore",
47
42
  "z3c.relationfield",
@@ -68,16 +63,17 @@ setup(
68
63
  "Development Status :: 5 - Production/Stable",
69
64
  "Environment :: Web Environment",
70
65
  "Framework :: Plone",
71
- "Framework :: Plone :: 6.0",
66
+ "Framework :: Plone :: 6.2",
72
67
  "Framework :: Plone :: Core",
73
68
  "Intended Audience :: Developers",
74
69
  "License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
75
70
  "Operating System :: OS Independent",
76
71
  "Programming Language :: Python",
77
- "Programming Language :: Python :: 3.8",
78
- "Programming Language :: Python :: 3.9",
79
72
  "Programming Language :: Python :: 3.10",
80
73
  "Programming Language :: Python :: 3.11",
74
+ "Programming Language :: Python :: 3.12",
75
+ "Programming Language :: Python :: 3.13",
76
+ "Programming Language :: Python :: 3.14",
81
77
  ],
82
78
  platforms="Any",
83
79
  project_urls={
@@ -2,22 +2,19 @@
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from functools import lru_cache
5
+ from importlib.metadata import distribution
6
+ from importlib.metadata import PackageNotFoundError
5
7
  from plone.api import portal
6
8
  from plone.api.exc import InvalidParameterError
7
9
  from plone.api.validation import required_parameters
10
+ from plone.base.interfaces import INonInstallable
11
+ from plone.base.utils import get_installer
8
12
  from Products.CMFPlone.controlpanel.browser.quickinstaller import InstallerView
9
- from Products.CMFPlone.interfaces import INonInstallable
10
- from Products.CMFPlone.utils import get_installer
11
13
  from Products.GenericSetup import EXTENSION
12
- from typing import Dict
13
- from typing import List
14
- from typing import Tuple
15
14
  from zope.component import getAllUtilitiesRegisteredFor
16
15
  from zope.globalrequest import getRequest
17
16
 
18
17
  import logging
19
- import pkg_resources
20
-
21
18
 
22
19
  logger = logging.getLogger("plone.api.addon")
23
20
 
@@ -38,8 +35,8 @@ __all__ = [
38
35
  class NonInstallableAddons:
39
36
  """Set of add-ons not available for installation."""
40
37
 
41
- profiles: List[str]
42
- products: List[str]
38
+ profiles: list[str]
39
+ products: list[str]
43
40
 
44
41
 
45
42
  @dataclass
@@ -51,14 +48,14 @@ class AddonInformation:
51
48
  title: str
52
49
  description: str
53
50
 
54
- upgrade_profiles: Dict
55
- other_profiles: List[List]
56
- install_profile: Dict
57
- uninstall_profile: Dict
51
+ upgrade_profiles: dict
52
+ other_profiles: list[list]
53
+ install_profile: dict
54
+ uninstall_profile: dict
58
55
  profile_type: str
59
- upgrade_info: Dict
56
+ upgrade_info: dict
60
57
  valid: bool
61
- flags: List[str]
58
+ flags: list[str]
62
59
 
63
60
  def __repr__(self) -> str:
64
61
  """Return a string representation of this object."""
@@ -97,7 +94,7 @@ def _get_non_installable_addons() -> NonInstallableAddons:
97
94
 
98
95
 
99
96
  @lru_cache(maxsize=1)
100
- def _cached_addons() -> Tuple[Tuple[str, AddonInformation]]:
97
+ def _cached_addons() -> tuple[tuple[str, AddonInformation]]:
101
98
  """Return information about add-ons in this installation.
102
99
 
103
100
  :returns: Tuple of tuples with add-on id and AddonInformation.
@@ -197,7 +194,7 @@ def _update_addon_info(
197
194
  return addon
198
195
 
199
196
 
200
- def _get_addons() -> List[AddonInformation]:
197
+ def _get_addons() -> list[AddonInformation]:
201
198
  """Return an updated list of add-on information.
202
199
 
203
200
  :returns: List of AddonInformation.
@@ -211,7 +208,7 @@ def _get_addons() -> List[AddonInformation]:
211
208
  return result
212
209
 
213
210
 
214
- def get_addons(limit: str = "") -> List[AddonInformation]:
211
+ def get_addons(limit: str = "") -> list[AddonInformation]:
215
212
  """List add-ons in this Plone site.
216
213
 
217
214
  :param limit: Limit list of add-ons.
@@ -238,7 +235,7 @@ def get_addons(limit: str = "") -> List[AddonInformation]:
238
235
  return addons
239
236
 
240
237
 
241
- def get_addon_ids(limit: str = "") -> List[str]:
238
+ def get_addon_ids(limit: str = "") -> list[str]:
242
239
  """List add-ons ids in this Plone site.
243
240
 
244
241
  :param limit: Limit list of add-ons.
@@ -258,9 +255,9 @@ def get_addon_ids(limit: str = "") -> List[str]:
258
255
  def get_version(addon: str) -> str:
259
256
  """Return the version of the product (package)."""
260
257
  try:
261
- dist = pkg_resources.get_distribution(addon)
258
+ dist = distribution(addon)
262
259
  return dist.version
263
- except pkg_resources.DistributionNotFound:
260
+ except PackageNotFoundError:
264
261
  if "." in addon:
265
262
  return ""
266
263
  return get_version(f"Products.{addon}")
@@ -25,7 +25,6 @@ from zope.interface import providedBy
25
25
  import transaction
26
26
  import uuid
27
27
 
28
-
29
28
  _marker = []
30
29
 
31
30
  # Maximum number of attempts to generate a unique random ID
@@ -18,7 +18,6 @@ from zope.globalrequest import getRequest
18
18
  import traceback
19
19
  import Zope2
20
20
 
21
-
22
21
  IS_TEST = None
23
22
 
24
23
 
@@ -24,7 +24,6 @@ from zope.schema.interfaces import IVocabularyFactory
24
24
  import datetime as dtime
25
25
  import re
26
26
 
27
-
28
27
  logger = getLogger("plone.api.portal")
29
28
 
30
29
  try:
@@ -26,7 +26,6 @@ from zope.lifecycleevent import modified
26
26
 
27
27
  import logging
28
28
 
29
-
30
29
  try:
31
30
  distribution("plone.app.iterate")
32
31
  except PackageNotFoundError:
@@ -257,6 +257,109 @@ api.portal.send_email(
257
257
  % 'attachment; filename="report.xml',
258
258
  % payloads[1]['Content-Disposition']
259
259
  % )
260
+ % mailhost.messages.clear()
261
+
262
+ The following code is a more complex example that constructs an email with a file attachment, HTML, plain text, and mail headers to control the mail response.
263
+
264
+ ```python
265
+ from email.encoders import encode_base64
266
+ from email.header import Header
267
+ from email.mime.base import MIMEBase
268
+ from email.mime.multipart import MIMEMultipart
269
+ from email.mime.text import MIMEText
270
+ from plone import api
271
+
272
+ # we need a message part to bundle the HTML and Plain Text
273
+ textmsgpart = MIMEMultipart("alternative")
274
+
275
+ # create plain text part of email
276
+ plaintextpart = MIMEText("Fill out your plain text", "plain", "utf-8")
277
+
278
+ # create html text of email
279
+ html = "<html><body><p>fill out your text in HTML</p></body></html>"
280
+ htmlpart = MIMEText(html, "html", "utf-8")
281
+
282
+ # bundle the parts
283
+ textmsgpart.attach(plaintextpart)
284
+ textmsgpart.attach(htmlpart)
285
+
286
+ # handle the file attachment
287
+
288
+ # Create a dummy PDF as NamedBlobFile
289
+ from plone.namedfile.file import NamedBlobFile
290
+
291
+ # Minimal PDF content (a valid but nearly empty PDF)
292
+ pdf_content = b"""%PDF-1.4
293
+ 1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
294
+ 2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj
295
+ 3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]/Contents 4 0 R>>endobj
296
+ 4 0 obj<</Length 44>>stream
297
+ BT /F1 12 Tf 100 700 Td (Hello World) Tj ET
298
+ endstream endobj
299
+ xref
300
+ 0 5
301
+ 0000000000 65535 f
302
+ 0000000009 00000 n
303
+ 0000000056 00000 n
304
+ 0000000115 00000 n
305
+ 0000000214 00000 n
306
+ trailer<</Size 5/Root 1 0 R>>
307
+ startxref
308
+ 315
309
+ %%EOF"""
310
+
311
+ attachment = NamedBlobFile(
312
+ data=pdf_content,
313
+ contentType='application/pdf',
314
+ filename='document.pdf'
315
+ )
316
+
317
+ filepart = MIMEBase("application", "pdf")
318
+ filepart.set_payload(attachment.data)
319
+ encode_base64(filepart)
320
+ filepart.add_header(
321
+ "Content-Disposition",
322
+ "attachment",
323
+ filename=(Header(attachment.filename, "utf-8").encode()),
324
+ )
325
+
326
+ # we need a mixed Multipart Message to bundle the text bundle and the attachment
327
+ msg = MIMEMultipart("mixed")
328
+ msg["From"] = "sender@abc"
329
+ msg["To"] = "recipient@zzz"
330
+ msg["Subject"] = "The Mail Subject"
331
+ msg["Reply-To"] = "community@plone.org"
332
+ msg["Return-Path"] = "error@xxx"
333
+
334
+ # add the text message bundle
335
+ msg.attach(textmsgpart)
336
+
337
+ # add the file attachment
338
+ msg.attach(filepart)
339
+
340
+ # send with plone.api
341
+ api.portal.send_email(
342
+ sender=msg["From"],
343
+ recipient=msg["To"],
344
+ subject=msg["Subject"],
345
+ body=msg.as_string()
346
+ )
347
+ ```
348
+
349
+ % invisible-code-block: python
350
+ %
351
+ % self.assertEqual(len(mailhost.messages), 1)
352
+ %
353
+ % msg = message_from_bytes(mailhost.messages[0])
354
+ % payloads = msg.get_payload()
355
+ % self.assertTrue(len(payloads) == 2)
356
+ % self.assertTrue(msg['Reply-To'] == 'community@plone.org')
357
+ % text_payloads = payloads[0].get_payload()
358
+ % self.assertEqual(len(text_payloads), 2)
359
+ % self.assertIn(
360
+ % 'attachment; filename',
361
+ % payloads[1]['Content-Disposition']
362
+ % )
260
363
  % api.portal.PRINTINGMAILHOST_ENABLED = False
261
364
  % mailhost.reset()
262
365
 
@@ -6,7 +6,6 @@ from plone.api.tests.base import INTEGRATION_TESTING
6
6
 
7
7
  import unittest
8
8
 
9
-
10
9
  ADDON = "plone.session"
11
10
 
12
11