Skip to content

Commit 5600f93

Browse files
authored
Merge pull request #55 from jgosmann/upgrades
Upgrades
2 parents b2aa0a3 + b098888 commit 5600f93

7 files changed

Lines changed: 2003 additions & 1322 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ name: CI and release pipeline
33
on:
44
pull_request:
55
push:
6-
branches: ["main", "develop"]
7-
tags: ["*"]
6+
branches: [ "main", "develop" ]
7+
tags: [ "*" ]
88

99
jobs:
1010
static-analysis:
@@ -19,11 +19,12 @@ jobs:
1919
- name: Check static typing
2020
run: poetry run mypy dmarc_metrics_exporter
2121
steps:
22-
- uses: actions/checkout@v4
23-
- run: git fetch --no-tags --prune --depth=1 origin +refs/heads/main:refs/remotes/origin/main
22+
- uses: actions/checkout@v6
23+
- run: git fetch --no-tags --prune --depth=1 origin
24+
+refs/heads/main:refs/remotes/origin/main
2425

2526
- name: Set up Python
26-
uses: actions/setup-python@v5.0.0
27+
uses: actions/setup-python@v6.2.0
2728
with:
2829
python-version: "3.13"
2930

@@ -45,13 +46,13 @@ jobs:
4546
runs-on: ubuntu-latest
4647
strategy:
4748
matrix:
48-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
49+
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]
4950

5051
steps:
51-
- uses: actions/checkout@v4
52+
- uses: actions/checkout@v6
5253

5354
- name: Set up Python ${{ matrix.python-version }}
54-
uses: actions/setup-python@v5.0.0
55+
uses: actions/setup-python@v6.2.0
5556
with:
5657
python-version: ${{ matrix.python-version }}
5758

@@ -62,27 +63,28 @@ jobs:
6263
- uses: actions/cache@v5.0.0
6364
with:
6465
path: .venv
65-
key: ${{ runner.os }}-py${{ matrix.python-version }}-venv-${{ hashFiles('pyproject.toml') }}
66+
key: ${{ runner.os }}-py${{ matrix.python-version }}-venv-${{
67+
hashFiles('pyproject.toml') }}
6668
restore-keys: |
6769
${{ runner.os }}-py${{ matrix.python-version }}-venv-
6870
- uses: ./.github/actions/setup-project
6971
- name: Start required services (Greenmail)
7072
run: docker compose up -d
7173
- name: Test and measure coverage with pytest
7274
run: poetry run pytest --verbose --cov=dmarc_metrics_exporter --cov-report=xml
73-
- uses: codecov/codecov-action@v4
75+
- uses: codecov/codecov-action@v6
7476
env:
7577
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
7678

7779
release:
7880
runs-on: ubuntu-latest
79-
needs: [static-analysis, test]
81+
needs: [ static-analysis, test ]
8082
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
8183
steps:
82-
- uses: actions/checkout@v4
84+
- uses: actions/checkout@v6
8385

8486
- name: Set up Python
85-
uses: actions/setup-python@v5.0.0
87+
uses: actions/setup-python@v6.2.0
8688
with:
8789
python-version: "3.13"
8890

@@ -110,7 +112,9 @@ jobs:
110112

111113
- name: Extract changelog
112114
id: changelog
113-
run: sed -E -n '/^\[${{ steps.version.outputs.version }}\]/,/^\[[0-9\.]+\]/{/^\[[0-9\.]+\]|^-+$/!p;}' CHANGELOG.rst > release-body.rst
115+
run: sed -E -n '/^\[${{ steps.version.outputs.version
116+
}}\]/,/^\[[0-9\.]+\]/{/^\[[0-9\.]+\]|^-+$/!p;}' CHANGELOG.rst >
117+
release-body.rst
114118

115119
- uses: docker://pandoc/core:3
116120
with:
@@ -132,7 +136,7 @@ jobs:
132136
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
133137

134138
docker-image:
135-
needs: [release]
139+
needs: [ release ]
136140
uses: "jgosmann/dmarc-metrics-exporter/.github/workflows/docker-publish.yml@main"
137141
with:
138142
version: ${{ github.ref }}

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ Added
1313
^^^^^
1414

1515
* Support for DMARC 2.0 reports from [draft-ietf-dmarc-aggregate-reporting-32](https://datatracker.ietf.org/doc/draft-ietf-dmarc-aggregate-reporting/32/).
16+
* Support for Python 3.14.
17+
18+
Changed
19+
^^^^^^^
20+
21+
* Drop support for Python 3.9.
1622

1723

1824
[1.2.0] - 2024-10-15

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
FROM --platform=linux/amd64 python:3.13-slim AS builder
1+
FROM --platform=linux/amd64 python:3.14-slim AS builder
22
ARG version
33

44
# install python package in venv
55
RUN python3 -m venv venv && \
66
venv/bin/pip3 --disable-pip-version-check install dmarc-metrics-exporter==${version}
77

8-
FROM python:3.13-alpine AS runner
8+
FROM python:3.14-alpine AS runner
99

1010
# adduser and directories
1111
RUN addgroup --system --gid 1000 dmarc-metrics && \
Lines changed: 30 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,20 @@
11
import json
22
from pathlib import Path
3-
from typing import Any, List, Tuple
43

5-
from dataclasses_serialization.json import JSONSerializer
4+
from pydantic import BaseModel, TypeAdapter
65

7-
from dmarc_metrics_exporter.dmarc_event import Disposition, Meta
6+
from dmarc_metrics_exporter.dmarc_event import Meta
87

98
from .dmarc_metrics import DmarcMetrics, DmarcMetricsCollection, InvalidMeta
109

10+
_Meta = TypeAdapter(Meta)
11+
_DmarcMetrics = TypeAdapter(DmarcMetrics)
12+
_InvalidMeta = TypeAdapter(InvalidMeta)
1113

12-
# false positive, pylint: disable=no-value-for-parameter
13-
@JSONSerializer.register_serializer(Disposition)
14-
def disposition_serializer(disposition: Disposition) -> str:
15-
return disposition.value
1614

17-
18-
@JSONSerializer.register_serializer(DmarcMetricsCollection)
19-
def dmarc_metrics_collection_serializer(
20-
metrics: DmarcMetricsCollection,
21-
) -> List[Tuple[Any, Any]]:
22-
return JSONSerializer.serialize(
23-
{
24-
"metrics": [list(item) for item in metrics.items()],
25-
"invalid_reports": [list(item) for item in metrics.invalid_reports.items()],
26-
}
27-
)
28-
29-
30-
@JSONSerializer.register_deserializer(Disposition)
31-
def disposition_deserializer(_cls, obj: str) -> Disposition:
32-
return Disposition(obj)
33-
34-
35-
@JSONSerializer.register_deserializer(DmarcMetricsCollection)
36-
def dmarc_metrics_collection_deserializer(_cls, obj) -> DmarcMetricsCollection:
37-
is_old_format = isinstance(obj, list)
38-
if is_old_format:
39-
obj = {"metrics": obj}
40-
return DmarcMetricsCollection(
41-
dict(
42-
(
43-
JSONSerializer.deserialize(Meta, meta),
44-
JSONSerializer.deserialize(DmarcMetrics, metrics),
45-
)
46-
for meta, metrics in obj.get("metrics", tuple())
47-
),
48-
dict(
49-
(JSONSerializer.deserialize(InvalidMeta, meta), count)
50-
for meta, count in obj.get("invalid_reports", tuple())
51-
),
52-
)
15+
class _SerializationModel(BaseModel):
16+
metrics: list[tuple[Meta, DmarcMetrics]]
17+
invalid_reports: list[tuple[InvalidMeta, int]] = []
5318

5419

5520
class MetricsPersister:
@@ -59,10 +24,30 @@ def __init__(self, path: Path):
5924
def load(self) -> DmarcMetricsCollection:
6025
try:
6126
with open(self.path, "r", encoding="utf-8") as f:
62-
return JSONSerializer.deserialize(DmarcMetricsCollection, json.load(f))
27+
obj = json.load(f)
28+
is_old_format = isinstance(obj, list)
29+
if is_old_format:
30+
obj = {"metrics": obj}
31+
model = _SerializationModel(**obj)
32+
return DmarcMetricsCollection(
33+
metrics={
34+
_Meta.validate_python(meta): _DmarcMetrics.validate_python(
35+
metrics
36+
)
37+
for meta, metrics in model.metrics
38+
},
39+
invalid_reports={
40+
_InvalidMeta.validate_python(meta): count
41+
for meta, count in model.invalid_reports
42+
},
43+
)
6344
except FileNotFoundError:
6445
return DmarcMetricsCollection()
6546

6647
def save(self, metrics: DmarcMetricsCollection):
48+
model = _SerializationModel(
49+
metrics=[tuple(item) for item in metrics.items()],
50+
invalid_reports=[(k, v) for k, v in metrics.invalid_reports.items()],
51+
)
6752
with open(self.path, "w", encoding="utf-8") as f:
68-
json.dump(JSONSerializer.serialize(metrics), f)
53+
f.write(model.model_dump_json())

dmarc_metrics_exporter/tests/test_metrics_persister.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,60 @@ def test_loads_old_format(tmp_path):
7575
)
7676

7777

78+
def test_loads_old_format2(tmp_path):
79+
metrics_db = tmp_path / "metrics.db"
80+
metrics_db.write_text(
81+
"""{
82+
"metrics": [
83+
[
84+
{
85+
"reporter": "google.com",
86+
"from_domain": "mydomain.de",
87+
"dkim_domain": "dkim-domain.org",
88+
"spf_domain": "spf-domain.org"
89+
}, {
90+
"total_count": 42,
91+
"disposition_counts": {"quarantine": 4},
92+
"dmarc_compliant_count": 24,
93+
"dkim_pass_count": 10,
94+
"spf_pass_count": 8,
95+
"dkim_aligned_count": 5,
96+
"spf_aligned_count": 4
97+
}
98+
]
99+
],
100+
"invalid_reports": [
101+
[
102+
{"from_email": "invalid <invalid@example.invalid>"},
103+
1
104+
]
105+
]
106+
}"""
107+
)
108+
persister = MetricsPersister(metrics_db)
109+
assert persister.load() == DmarcMetricsCollection(
110+
metrics={
111+
Meta(
112+
reporter="google.com",
113+
from_domain="mydomain.de",
114+
dkim_domain="dkim-domain.org",
115+
spf_domain="spf-domain.org",
116+
): DmarcMetrics(
117+
total_count=42,
118+
disposition_counts={Disposition.QUARANTINE: 4},
119+
dmarc_compliant_count=24,
120+
dkim_aligned_count=5,
121+
dkim_pass_count=10,
122+
spf_aligned_count=4,
123+
spf_pass_count=8,
124+
)
125+
},
126+
invalid_reports={
127+
InvalidMeta(from_email="invalid <invalid@example.invalid>"): 1,
128+
},
129+
)
130+
131+
78132
def test_returns_newly_initialized_metrics_if_db_is_non_existent(tmp_path):
79133
metrics_db = tmp_path / "metrics.db"
80134
persister = MetricsPersister(metrics_db)

0 commit comments

Comments
 (0)