pintos-utils 1.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.
- pintos_utils-1.0.0/.gitignore +17 -0
- pintos_utils-1.0.0/LICENSE +21 -0
- pintos_utils-1.0.0/PKG-INFO +78 -0
- pintos_utils-1.0.0/README.md +64 -0
- pintos_utils-1.0.0/docs/email.md +171 -0
- pintos_utils-1.0.0/docs/releases.md +112 -0
- pintos_utils-1.0.0/pyproject.toml +75 -0
- pintos_utils-1.0.0/src/pintos_utils/__init__.py +1 -0
- pintos_utils-1.0.0/src/pintos_utils/email/__init__.py +29 -0
- pintos_utils-1.0.0/src/pintos_utils/email/contracts.py +15 -0
- pintos_utils-1.0.0/src/pintos_utils/email/exceptions.py +32 -0
- pintos_utils-1.0.0/src/pintos_utils/email/models.py +119 -0
- pintos_utils-1.0.0/src/pintos_utils/email/providers/__init__.py +1 -0
- pintos_utils-1.0.0/src/pintos_utils/email/providers/gmail/__init__.py +16 -0
- pintos_utils-1.0.0/src/pintos_utils/email/providers/gmail/_mime.py +46 -0
- pintos_utils-1.0.0/src/pintos_utils/email/providers/gmail/sender.py +153 -0
- pintos_utils-1.0.0/src/pintos_utils/py.typed +0 -0
- pintos_utils-1.0.0/tests/unit/email/fakes.py +18 -0
- pintos_utils-1.0.0/tests/unit/email/providers/gmail/test_mime.py +91 -0
- pintos_utils-1.0.0/tests/unit/email/providers/gmail/test_sender.py +238 -0
- pintos_utils-1.0.0/tests/unit/email/test_contract.py +57 -0
- pintos_utils-1.0.0/tests/unit/email/test_exceptions.py +32 -0
- pintos_utils-1.0.0/tests/unit/email/test_models.py +117 -0
- pintos_utils-1.0.0/tests/unit/test_release_preflight.py +135 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivaney Vieira de Sales
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pintos-utils
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Reusable, provider-neutral utilities for Python applications.
|
|
5
|
+
Project-URL: Source, https://github.com/PINTOS-LTDA/python-utils
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Provides-Extra: gmail
|
|
10
|
+
Requires-Dist: google-api-python-client<3,>=2.150; extra == 'gmail'
|
|
11
|
+
Requires-Dist: google-auth-httplib2<1,>=0.2; extra == 'gmail'
|
|
12
|
+
Requires-Dist: google-auth<3,>=2.35; extra == 'gmail'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# pintos-utils
|
|
16
|
+
|
|
17
|
+
Reusable Python utilities for application projects. The package is framework
|
|
18
|
+
independent and keeps provider-specific integrations optional.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install the published base package from PyPI:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m pip install pintos-utils
|
|
26
|
+
uv add pintos-utils
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For Gmail support and its optional Google dependencies:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m pip install "pintos-utils[gmail]"
|
|
33
|
+
uv add "pintos-utils[gmail]"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Install the base package from a local checkout:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv pip install .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For editable development:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Install the Gmail integration and its optional dependencies when needed:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv pip install "pintos-utils[gmail] @ git+ssh://git@<git-host>/<org>/pintos-utils.git@<tag-or-commit>"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The base package has no third-party runtime dependencies. Configure SSH access
|
|
55
|
+
using the consuming environment; do not put credentials in the Git URL.
|
|
56
|
+
|
|
57
|
+
## Email module
|
|
58
|
+
|
|
59
|
+
See [docs/email.md](docs/email.md) for the provider-neutral API, Gmail OAuth
|
|
60
|
+
ownership, error behavior, and installation examples.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
Use Python 3.11 and uv to create the environment and install development tools:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv sync --group dev
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Run the tests, lint, formatting check, type check, and package build:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv run pytest --cov=pintos_utils --cov-report=term-missing
|
|
74
|
+
uv run ruff check .
|
|
75
|
+
uv run ruff format --check .
|
|
76
|
+
uv run mypy src
|
|
77
|
+
uv build
|
|
78
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# pintos-utils
|
|
2
|
+
|
|
3
|
+
Reusable Python utilities for application projects. The package is framework
|
|
4
|
+
independent and keeps provider-specific integrations optional.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Install the published base package from PyPI:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python -m pip install pintos-utils
|
|
12
|
+
uv add pintos-utils
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For Gmail support and its optional Google dependencies:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
python -m pip install "pintos-utils[gmail]"
|
|
19
|
+
uv add "pintos-utils[gmail]"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Install the base package from a local checkout:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For editable development:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install the Gmail integration and its optional dependencies when needed:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv pip install "pintos-utils[gmail] @ git+ssh://git@<git-host>/<org>/pintos-utils.git@<tag-or-commit>"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The base package has no third-party runtime dependencies. Configure SSH access
|
|
41
|
+
using the consuming environment; do not put credentials in the Git URL.
|
|
42
|
+
|
|
43
|
+
## Email module
|
|
44
|
+
|
|
45
|
+
See [docs/email.md](docs/email.md) for the provider-neutral API, Gmail OAuth
|
|
46
|
+
ownership, error behavior, and installation examples.
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
Use Python 3.11 and uv to create the environment and install development tools:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv sync --group dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run the tests, lint, formatting check, type check, and package build:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv run pytest --cov=pintos_utils --cov-report=term-missing
|
|
60
|
+
uv run ruff check .
|
|
61
|
+
uv run ruff format --check .
|
|
62
|
+
uv run mypy src
|
|
63
|
+
uv build
|
|
64
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Email API
|
|
2
|
+
|
|
3
|
+
`pintos-utils` exposes provider-neutral message models and the `EmailSender`
|
|
4
|
+
contract from `pintos_utils.email`. Gmail is an optional adapter. Applications
|
|
5
|
+
can inject another implementation of the same contract without changing the
|
|
6
|
+
code that builds messages.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Install the published base package from PyPI:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python -m pip install pintos-utils
|
|
14
|
+
uv add pintos-utils
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For Gmail support, request the optional extra:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m pip install "pintos-utils[gmail]"
|
|
21
|
+
uv add "pintos-utils[gmail]"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Install the base package from a checkout with pip or uv:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python -m pip install .
|
|
28
|
+
uv pip install .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For editable development with Gmail support:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python -m pip install -e ".[gmail]"
|
|
35
|
+
uv pip install -e ".[gmail]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The base package has no third-party runtime dependencies. The `gmail` extra
|
|
39
|
+
installs the Google API and authentication libraries. Importing
|
|
40
|
+
`pintos_utils.email` does not load them. Import the adapter explicitly when the
|
|
41
|
+
extra is installed:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pintos_utils.email.providers.gmail import GmailEmailSender
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For an authorized private SSH Git repository, use a reviewed tag or, for a
|
|
48
|
+
reproducible production pin, a full commit hash:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python -m pip install "pintos-utils[gmail] @ git+ssh://git@<git-host>/<org>/pintos-utils.git@<full-commit-sha>"
|
|
52
|
+
uv add "pintos-utils[gmail] @ git+ssh://git@<git-host>/<org>/pintos-utils.git@<full-commit-sha>"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Replace the placeholders with the repository location and revision. SSH agent,
|
|
56
|
+
deploy key, or the environment's Git credential helper must already grant
|
|
57
|
+
access. Do not put passwords or tokens in the URL or committed dependency
|
|
58
|
+
configuration.
|
|
59
|
+
|
|
60
|
+
## Public contract
|
|
61
|
+
|
|
62
|
+
The provider-neutral API exports:
|
|
63
|
+
|
|
64
|
+
- `EmailAddress(address, display_name=None)`
|
|
65
|
+
- `EmailAttachment(filename, content, content_type=None)`
|
|
66
|
+
- `EmailMessage(sender, subject, to=(), cc=(), bcc=(), text_body=None, html_body=None, attachments=())`
|
|
67
|
+
- `EmailSender`, a structural protocol with `send(message, /) -> SendResult`
|
|
68
|
+
- `SendResult(provider, provider_message_id=None)`
|
|
69
|
+
- `EmailError`, `EmailValidationError`, `EmailConfigurationError`,
|
|
70
|
+
`EmailAuthenticationError`, and `EmailProviderError`
|
|
71
|
+
|
|
72
|
+
Models are frozen dataclasses. Recipient and attachment collections are stored
|
|
73
|
+
as tuples. At least one recipient across To, CC, and BCC and at least one body
|
|
74
|
+
format are required. Supplying both text and HTML sends them as alternatives.
|
|
75
|
+
Header line breaks are rejected. Attachments are passed as in-memory bytes; the
|
|
76
|
+
library does not read paths.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pintos_utils.email import EmailAddress, EmailAttachment, EmailMessage
|
|
80
|
+
|
|
81
|
+
message = EmailMessage(
|
|
82
|
+
sender=EmailAddress("reports@example.com", "Reports"),
|
|
83
|
+
to=(EmailAddress("person@example.net"),),
|
|
84
|
+
cc=(EmailAddress("team@example.net"),),
|
|
85
|
+
bcc=(EmailAddress("archive@example.net"),),
|
|
86
|
+
subject="Monthly report",
|
|
87
|
+
text_body="The report is attached.",
|
|
88
|
+
html_body="<p>The report is attached.</p>",
|
|
89
|
+
attachments=(
|
|
90
|
+
EmailAttachment("report.csv", csv_bytes, "text/csv"),
|
|
91
|
+
EmailAttachment("chart.png", image_bytes, "image/png"),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Gmail OAuth and sending
|
|
97
|
+
|
|
98
|
+
The application owns OAuth client configuration, the initial consent flow,
|
|
99
|
+
refresh-token persistence, and secret storage. It passes configured Google
|
|
100
|
+
credentials to the adapter. Request the narrow
|
|
101
|
+
`https://www.googleapis.com/auth/gmail.send` scope, available as
|
|
102
|
+
`GMAIL_SEND_SCOPE`. Google Auth refreshes an invalid access token when a refresh
|
|
103
|
+
token is available; this library does not start an interactive OAuth flow or
|
|
104
|
+
persist tokens.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from google.oauth2.credentials import Credentials
|
|
108
|
+
|
|
109
|
+
from pintos_utils.email.providers.gmail import (
|
|
110
|
+
GMAIL_SEND_SCOPE,
|
|
111
|
+
GmailEmailSender,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
credentials = Credentials(
|
|
115
|
+
token=access_token,
|
|
116
|
+
refresh_token=refresh_token,
|
|
117
|
+
token_uri=token_uri,
|
|
118
|
+
client_id=client_id,
|
|
119
|
+
client_secret=client_secret,
|
|
120
|
+
scopes=[GMAIL_SEND_SCOPE],
|
|
121
|
+
)
|
|
122
|
+
sender = GmailEmailSender(credentials)
|
|
123
|
+
result = sender.send(message)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`SendResult` means Gmail accepted the request. It does not confirm final delivery
|
|
127
|
+
to every recipient. The result includes `provider == "gmail"` and may include an
|
|
128
|
+
opaque `provider_message_id`.
|
|
129
|
+
|
|
130
|
+
## Errors and retry policy
|
|
131
|
+
|
|
132
|
+
Invalid local message data raises `EmailValidationError`. Missing or unusable
|
|
133
|
+
credentials raise `EmailConfigurationError` or `EmailAuthenticationError`.
|
|
134
|
+
Provider or transport failures raise `EmailProviderError`, with a stable
|
|
135
|
+
`provider` value and `retryable: bool | None` advisory. The original SDK or
|
|
136
|
+
transport error is chained as `__cause__`; its text and raw response are not
|
|
137
|
+
copied into the library exception message.
|
|
138
|
+
|
|
139
|
+
For Gmail, known temporary quota/backend responses are classified as retryable,
|
|
140
|
+
known permanent rejections as non-retryable, and unclassified or transport
|
|
141
|
+
failures as unknown. The adapter never retries automatically. A timeout can
|
|
142
|
+
occur after Gmail accepted the message, so the consuming application must
|
|
143
|
+
consider duplicate-send risk before retrying.
|
|
144
|
+
|
|
145
|
+
All providers implement the same small contract:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from pintos_utils.email import EmailMessage, EmailSender, SendResult
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def send_report(sender: EmailSender, message: EmailMessage) -> SendResult:
|
|
152
|
+
return sender.send(message)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The Gmail SDK, request objects, MIME payload, HTTP status codes, and provider
|
|
156
|
+
message identifier details remain inside the adapter boundary.
|
|
157
|
+
|
|
158
|
+
## Versioning
|
|
159
|
+
|
|
160
|
+
The package uses Semantic Versioning. Version `1.0.0` is the first stable public
|
|
161
|
+
API release, after implementation, validation, and API compatibility review.
|
|
162
|
+
Compatible fixes increment patch, compatible additions increment minor, and
|
|
163
|
+
breaking public API changes increment major.
|
|
164
|
+
|
|
165
|
+
The compatibility boundary includes the names and signatures exported by
|
|
166
|
+
`pintos_utils.email`, the `GmailEmailSender` and `GMAIL_SEND_SCOPE` adapter
|
|
167
|
+
exports, validation behavior, exception categories, retryability semantics, and
|
|
168
|
+
the meaning of `SendResult`. Gmail request objects, HTTP parsing, MIME assembly,
|
|
169
|
+
and Google SDK types remain implementation details. Future provider adapters
|
|
170
|
+
can be added without changing the shared contract; a change to that contract's
|
|
171
|
+
existing behavior requires a major version.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Release Runbook
|
|
2
|
+
|
|
3
|
+
This runbook defines the checks and external settings for publishing `pintos-utils` to PyPI through GitHub Actions Trusted Publishing. The workflow does not use a stored PyPI API token.
|
|
4
|
+
|
|
5
|
+
## Verified Release Identity
|
|
6
|
+
|
|
7
|
+
| Setting | Required value |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| GitHub repository | `PINTOS-LTDA/python-utils` |
|
|
10
|
+
| Repository visibility checked | Public (checked 2026-10-21) |
|
|
11
|
+
| PyPI distribution | `pintos-utils` |
|
|
12
|
+
| Source URL | `https://github.com/PINTOS-LTDA/python-utils` |
|
|
13
|
+
| Release workflow file | `.github/workflows/release.yml` (PyPI field: `release.yml`) |
|
|
14
|
+
| GitHub environment and PyPI environment claim | `pypi` |
|
|
15
|
+
| Stable release tag | `vMAJOR.MINOR.PATCH`, without prerelease suffixes |
|
|
16
|
+
| License | MIT; `Ivaney Vieira de Sales`, 2026 |
|
|
17
|
+
|
|
18
|
+
## Readiness Checks
|
|
19
|
+
|
|
20
|
+
### GitHub environment approval
|
|
21
|
+
|
|
22
|
+
The repository was confirmed public on 2026-10-21. GitHub's required-reviewer availability depends on repository visibility and the plan. Confirm in repository settings that the `pypi` environment offers required reviewers before enabling release publication. GitHub documents required reviewers for public repositories on Free, Pro, and Team plans; if the setting is unavailable, stop and resolve account-plan support rather than removing the approval gate. Select the required reviewer(s) from authorized project maintainers; this runbook does not invent reviewer identities.
|
|
23
|
+
|
|
24
|
+
Configure the `pypi` environment to require approval from `ivaneyvieira`, restrict deployment to the intended stable release tags when supported, and reject a controlled deployment during the no-upload check. This is the sole reviewer identity supplied so far; leave GitHub's optional self-review prevention disabled unless another reviewer is added, or the only reviewer could be unable to approve a release they initiate.
|
|
25
|
+
|
|
26
|
+
Create and protect the `pypi` environment before configuring PyPI's pending publisher. GitHub automatically creates a missing environment when a workflow references it, but that environment has no protection rules by default. Do not enable the PyPI publisher until the named environment is visible in repository settings with required reviewers and the intended stable-tag restriction. See [GitHub's environment management behavior](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments).
|
|
27
|
+
|
|
28
|
+
### PyPI name and pending publisher
|
|
29
|
+
|
|
30
|
+
Review `pintos-utils` against [PyPI's project-name guidance](https://docs.pypi.org/project-management/choosing-a-name/) before configuring it. A `404` from the project JSON endpoint does **not** prove that the name is available. A pending publisher does not reserve a project name; another party may register it before the first authorized upload. PyPI's first-use project creation and publisher authentication are authoritative. If PyPI rejects project creation or publisher authentication, stop before upload and ask the project owner to review the name and publisher settings. Do not rename the distribution automatically.
|
|
31
|
+
|
|
32
|
+
For a package that does not yet exist on PyPI, configure the pending Trusted Publisher values below and let PyPI attempt project creation only during the first explicitly authorized publication. After PyPI creates the project, confirm that the same publisher is listed as the project's normal Trusted Publisher. Do not treat pending configuration as a reservation or proof of ownership.
|
|
33
|
+
|
|
34
|
+
### Trusted Publisher fields
|
|
35
|
+
|
|
36
|
+
Configure the publisher for the project `pintos-utils` with these exact claims:
|
|
37
|
+
|
|
38
|
+
| PyPI field | Value |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| Owner | `PINTOS-LTDA` |
|
|
41
|
+
| Repository | `python-utils` |
|
|
42
|
+
| Workflow filename | `release.yml` |
|
|
43
|
+
| Environment | `pypi` |
|
|
44
|
+
|
|
45
|
+
The effective PyPI publisher record must match the isolated `publish` job in `.github/workflows/release.yml`. Confirm that no additional publisher is configured. Only that job may have `id-token: write`, and no workflow may contain a static PyPI token.
|
|
46
|
+
|
|
47
|
+
## Workflow and Tag Rules
|
|
48
|
+
|
|
49
|
+
- Create a GitHub Release only for a reviewed stable tag in the exact form `vMAJOR.MINOR.PATCH`; its version must equal `[project].version` after removing `v`.
|
|
50
|
+
- The release workflow accepts a published, non-prerelease GitHub Release. Branch and pull request validation must not request OIDC or invoke the publisher.
|
|
51
|
+
- Protect creation and update of `v*` tags with a repository ruleset for authorized maintainers. The supplied maintainer identity is `ivaneyvieira`.
|
|
52
|
+
- Pin every third-party action to a reviewed full commit SHA. Keep a version comment beside each pin and review pins when changing an action.
|
|
53
|
+
- The first real PyPI upload requires an explicit release instruction and maintainer approval of the protected environment deployment.
|
|
54
|
+
|
|
55
|
+
Configure the repository in this order: review the PyPI name guidance; create and verify the protected `pypi` environment and stable-tag restrictions; add the `v*` tag ruleset; then configure PyPI's pending publisher with the exact fields above. This prevents a release from targeting a newly auto-created but unprotected environment.
|
|
56
|
+
|
|
57
|
+
## Local Checks Before a Release
|
|
58
|
+
|
|
59
|
+
Run the offline validation from the repository root:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv sync --locked --group dev
|
|
63
|
+
uv run pytest --cov=pintos_utils --cov-report=term-missing
|
|
64
|
+
uv run ruff check .
|
|
65
|
+
uv run ruff format --check .
|
|
66
|
+
uv run mypy src scripts/release_preflight.py
|
|
67
|
+
uv run pytest tests/unit/test_release_preflight.py
|
|
68
|
+
uv build --no-sources
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Inspect the wheel and sdist. Both must report package `pintos-utils`, the same version, Python `>=3.11`, the `gmail` extra, the source URL, MIT license metadata, and the `LICENSE` file. Confirm there is exactly one expected wheel and one sdist. Do not treat a successful local build as authorization to publish.
|
|
72
|
+
|
|
73
|
+
## Publisher Approval and Rejection Check
|
|
74
|
+
|
|
75
|
+
Before any production release, compare the actual PyPI Trusted Publisher settings with the `publish` job's OIDC claims: owner `PINTOS-LTDA`, repository `python-utils`, workflow `release.yml`, and environment `pypi`. Inspect the CI, quality, and release workflows to verify the OIDC permission boundary and absence of static credentials. Any mismatch or additional publisher blocks release readiness.
|
|
76
|
+
|
|
77
|
+
For a no-upload check, reject a controlled deployment at the protected `pypi` environment and confirm that PyPI received no distribution files. Record the date, workflow run, reviewer outcome, and no-upload observation in the release review record. Do not use an alternate project or distribution as an identity probe.
|
|
78
|
+
|
|
79
|
+
For an authorized release, the designated reviewer inspects the GitHub Release tag, successful preflight and quality jobs, and the two uploaded workflow artifacts before approving the `pypi` environment. Reject or cancel if any check is missing or unexpected. Rejection must prevent the publisher job from starting; confirm no files were uploaded before considering another attempt. Approval permits the isolated job to publish the already-built files; after completion, verify each file and its provenance as described below.
|
|
80
|
+
|
|
81
|
+
## Reviewed Action Pins
|
|
82
|
+
|
|
83
|
+
All third-party actions used by the release and quality workflows must use immutable full commit SHAs. These are the pins currently in the repository; review the upstream release and security notes when changing one.
|
|
84
|
+
|
|
85
|
+
| Action | Full commit SHA | Reviewed release |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `actions/checkout` | `3d3c42e5aac5ba805825da76410c181273ba90b1` | [v7.0.1](https://github.com/actions/checkout/releases/tag/v7.0.1) |
|
|
88
|
+
| `actions/setup-python` | `5fda3b95a4ea91299a34e894583c3862153e4b97` | [v7.0.0](https://github.com/actions/setup-python/releases/tag/v7.0.0) |
|
|
89
|
+
| `astral-sh/setup-uv` | `1c37ad07a6a961277cf70c0d37d6f313000f5884` | [v10.3.0](https://github.com/astral-sh/setup-uv/releases/tag/v10.3.0) |
|
|
90
|
+
| `actions/upload-artifact` | `cf430e030ddbb5b0abf93d22962f4752f3646cd9` | [v7.0.2](https://github.com/actions/upload-artifact/releases/tag/v7.0.2) |
|
|
91
|
+
| `actions/download-artifact` | `9000827ccba6bdab643e8b6fd33ac0654aef8333` | [v8.0.2](https://github.com/actions/download-artifact/releases/tag/v8.0.2) |
|
|
92
|
+
| `pypa/gh-action-pypi-publish` | `7245906fb61fa33a13554f9d5fb856fe61758086` | [v1.14.3](https://github.com/pypa/gh-action-pypi-publish/releases/tag/v1.14.3) |
|
|
93
|
+
|
|
94
|
+
## Failure Recovery
|
|
95
|
+
|
|
96
|
+
- **Tag/version mismatch, existing version, inconclusive version check, quality failure, or build failure**: stop before publication, fix the source of failure, then create a new reviewed release tag.
|
|
97
|
+
- **Rejected environment deployment**: no publisher job should run; confirm that no files were uploaded. A later attempt requires a fresh maintainer decision.
|
|
98
|
+
- **PyPI project creation or publisher authentication rejection**: stop before upload, confirm the PyPI name and exact publisher claims with the project owner, and do not switch to a static token or alternate name.
|
|
99
|
+
- **Upload or provenance failure**: inspect PyPI's release files and provenance for each file before any retry. PyPI can accept one distribution before a later upload fails, and published files cannot be replaced in place. Never retry an ambiguous upload blindly.
|
|
100
|
+
- **Missing or mismatched provenance**: do not record the release as complete. Compare each file's digest and attestation with the intended repository, tagged revision, workflow, and environment, then resolve the discrepancy before another release attempt.
|
|
101
|
+
|
|
102
|
+
## First Authorized Release Follow-up
|
|
103
|
+
|
|
104
|
+
After an explicitly authorized production upload, verify the PyPI project metadata, install the base package and Gmail extra in clean environments, and check the attestation for the wheel and sdist independently. Record evidence and any blockers in `specs/002-pypi-trusted-publishing/quickstart.md`. Do not mark these checks complete before a successful authorized release.
|
|
105
|
+
|
|
106
|
+
## References
|
|
107
|
+
|
|
108
|
+
- [PyPI Trusted Publisher setup](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
|
|
109
|
+
- [Create a project with a pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
|
|
110
|
+
- [PyPI project-name guidance](https://docs.pypi.org/project-management/choosing-a-name/)
|
|
111
|
+
- [GitHub deployment environments and required reviewers](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments)
|
|
112
|
+
- [PyPA publishing action](https://github.com/marketplace/actions/pypi-publish)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pintos-utils"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Reusable, provider-neutral utilities for Python applications."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Source = "https://github.com/PINTOS-LTDA/python-utils"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
gmail = [
|
|
20
|
+
"google-api-python-client>=2.150,<3",
|
|
21
|
+
"google-auth>=2.35,<3",
|
|
22
|
+
"google-auth-httplib2>=0.2,<1",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"mypy>=1.13,<2",
|
|
28
|
+
"pytest>=8.3,<10",
|
|
29
|
+
"pytest-cov>=6,<8",
|
|
30
|
+
"ruff>=0.9,<1",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/pintos_utils"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
37
|
+
"src/pintos_utils/py.typed" = "pintos_utils/py.typed"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.sdist]
|
|
40
|
+
include = [
|
|
41
|
+
"/LICENSE",
|
|
42
|
+
"/README.md",
|
|
43
|
+
"/docs",
|
|
44
|
+
"/pyproject.toml",
|
|
45
|
+
"/src",
|
|
46
|
+
"/tests",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests/unit"]
|
|
51
|
+
pythonpath = ["src", "tests/unit/email", "scripts"]
|
|
52
|
+
addopts = ["-ra"]
|
|
53
|
+
|
|
54
|
+
[tool.coverage.run]
|
|
55
|
+
branch = true
|
|
56
|
+
source = ["pintos_utils"]
|
|
57
|
+
|
|
58
|
+
[tool.coverage.report]
|
|
59
|
+
show_missing = true
|
|
60
|
+
skip_covered = true
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 88
|
|
64
|
+
target-version = "py311"
|
|
65
|
+
exclude = ["specs"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["B", "E", "F", "I", "UP"]
|
|
69
|
+
|
|
70
|
+
[tool.mypy]
|
|
71
|
+
python_version = "3.11"
|
|
72
|
+
mypy_path = "src"
|
|
73
|
+
strict = true
|
|
74
|
+
warn_unreachable = true
|
|
75
|
+
show_error_codes = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reusable utilities for Python applications."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Provider-neutral email models, contracts, and exceptions."""
|
|
2
|
+
|
|
3
|
+
from pintos_utils.email.contracts import EmailSender
|
|
4
|
+
from pintos_utils.email.exceptions import (
|
|
5
|
+
EmailAuthenticationError,
|
|
6
|
+
EmailConfigurationError,
|
|
7
|
+
EmailError,
|
|
8
|
+
EmailProviderError,
|
|
9
|
+
EmailValidationError,
|
|
10
|
+
)
|
|
11
|
+
from pintos_utils.email.models import (
|
|
12
|
+
EmailAddress,
|
|
13
|
+
EmailAttachment,
|
|
14
|
+
EmailMessage,
|
|
15
|
+
SendResult,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"EmailAddress",
|
|
20
|
+
"EmailAttachment",
|
|
21
|
+
"EmailAuthenticationError",
|
|
22
|
+
"EmailConfigurationError",
|
|
23
|
+
"EmailError",
|
|
24
|
+
"EmailMessage",
|
|
25
|
+
"EmailProviderError",
|
|
26
|
+
"EmailSender",
|
|
27
|
+
"EmailValidationError",
|
|
28
|
+
"SendResult",
|
|
29
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Provider-neutral email service contracts."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from pintos_utils.email.models import EmailMessage, SendResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EmailSender(Protocol):
|
|
9
|
+
"""Structural contract implemented by each email provider adapter."""
|
|
10
|
+
|
|
11
|
+
def send(self, message: EmailMessage, /) -> SendResult:
|
|
12
|
+
"""Submit one message and return the provider acceptance result."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["EmailSender"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Public exceptions raised by the email module."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EmailError(Exception):
|
|
5
|
+
"""Base class for errors exposed by ``pintos_utils.email``."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EmailValidationError(EmailError):
|
|
9
|
+
"""Raised when an email model contains invalid or unsafe values."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EmailConfigurationError(EmailError):
|
|
13
|
+
"""Raised when provider configuration is missing or unusable."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EmailAuthenticationError(EmailError):
|
|
17
|
+
"""Raised when provider credentials are rejected or cannot be refreshed."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EmailProviderError(EmailError):
|
|
21
|
+
"""Raised when a provider request fails or has an indeterminate outcome."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
message: str,
|
|
26
|
+
*,
|
|
27
|
+
provider: str,
|
|
28
|
+
retryable: bool | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.provider = provider
|
|
32
|
+
self.retryable = retryable
|