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.
@@ -0,0 +1,17 @@
1
+ .venv/
2
+ venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+ .coverage
9
+ .coverage.*
10
+ htmlcov/
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .mypy_cache/
14
+ .env
15
+ .env.*
16
+ !.env.example
17
+ .DS_Store
@@ -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