pytest-allure-host 0.1.1__py3-none-any.whl
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.
- pytest_allure_host/__init__.py +6 -0
- pytest_allure_host/__main__.py +4 -0
- pytest_allure_host/cli.py +104 -0
- pytest_allure_host/config.py +141 -0
- pytest_allure_host/plugin.py +127 -0
- pytest_allure_host/publisher.py +716 -0
- pytest_allure_host/utils.py +119 -0
- pytest_allure_host-0.1.1.dist-info/METADATA +305 -0
- pytest_allure_host-0.1.1.dist-info/RECORD +12 -0
- pytest_allure_host-0.1.1.dist-info/WHEEL +4 -0
- pytest_allure_host-0.1.1.dist-info/entry_points.txt +6 -0
- pytest_allure_host-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import argparse
|
4
|
+
import os
|
5
|
+
|
6
|
+
from .config import load_effective_config
|
7
|
+
from .publisher import plan_dry_run, preflight, publish
|
8
|
+
from .utils import PublishConfig, default_run_id
|
9
|
+
|
10
|
+
|
11
|
+
def parse_args() -> argparse.Namespace:
|
12
|
+
p = argparse.ArgumentParser("publish-allure")
|
13
|
+
p.add_argument("--config", help="Path to YAML config (optional)")
|
14
|
+
p.add_argument("--bucket")
|
15
|
+
p.add_argument("--prefix", default=None)
|
16
|
+
p.add_argument("--project")
|
17
|
+
p.add_argument("--branch", default=os.getenv("GIT_BRANCH", "main"))
|
18
|
+
p.add_argument(
|
19
|
+
"--run-id",
|
20
|
+
default=os.getenv("ALLURE_RUN_ID", default_run_id()),
|
21
|
+
)
|
22
|
+
p.add_argument("--cloudfront", default=os.getenv("ALLURE_CLOUDFRONT"))
|
23
|
+
p.add_argument("--results", default="allure-results")
|
24
|
+
p.add_argument("--report", default="allure-report")
|
25
|
+
p.add_argument("--ttl-days", type=int, default=None)
|
26
|
+
p.add_argument("--max-keep-runs", type=int, default=None)
|
27
|
+
p.add_argument(
|
28
|
+
"--s3-endpoint",
|
29
|
+
default=os.getenv("ALLURE_S3_ENDPOINT"),
|
30
|
+
help=("Custom S3 endpoint URL (e.g. http://localhost:4566 for LocalStack)"),
|
31
|
+
)
|
32
|
+
p.add_argument("--summary-json", default=None)
|
33
|
+
p.add_argument(
|
34
|
+
"--context-url",
|
35
|
+
default=os.getenv("ALLURE_CONTEXT_URL"),
|
36
|
+
help="Optional hyperlink giving change context (e.g. Jira ticket)",
|
37
|
+
)
|
38
|
+
p.add_argument("--dry-run", action="store_true", help="Plan only")
|
39
|
+
p.add_argument(
|
40
|
+
"--check",
|
41
|
+
action="store_true",
|
42
|
+
help="Run preflight checks (AWS, allure, inputs)",
|
43
|
+
)
|
44
|
+
return p.parse_args()
|
45
|
+
|
46
|
+
|
47
|
+
def main() -> int:
|
48
|
+
args = parse_args()
|
49
|
+
cli_overrides = {
|
50
|
+
"bucket": args.bucket,
|
51
|
+
"prefix": args.prefix,
|
52
|
+
"project": args.project,
|
53
|
+
"branch": args.branch,
|
54
|
+
"cloudfront": args.cloudfront,
|
55
|
+
"run_id": args.run_id,
|
56
|
+
"ttl_days": args.ttl_days,
|
57
|
+
"max_keep_runs": args.max_keep_runs,
|
58
|
+
"s3_endpoint": args.s3_endpoint,
|
59
|
+
"context_url": args.context_url,
|
60
|
+
}
|
61
|
+
effective = load_effective_config(cli_overrides, args.config)
|
62
|
+
missing = [k for k in ("bucket", "project") if not effective.get(k)]
|
63
|
+
if missing:
|
64
|
+
raise SystemExit(
|
65
|
+
f"Missing required config values: {', '.join(missing)}. Provide via CLI, env, or YAML." # noqa: E501
|
66
|
+
)
|
67
|
+
cfg = PublishConfig(
|
68
|
+
bucket=effective["bucket"],
|
69
|
+
prefix=effective.get("prefix") or "reports",
|
70
|
+
project=effective["project"],
|
71
|
+
branch=effective.get("branch") or args.branch,
|
72
|
+
run_id=effective.get("run_id") or args.run_id,
|
73
|
+
cloudfront_domain=effective.get("cloudfront"),
|
74
|
+
ttl_days=effective.get("ttl_days"),
|
75
|
+
max_keep_runs=effective.get("max_keep_runs"),
|
76
|
+
s3_endpoint=effective.get("s3_endpoint"),
|
77
|
+
context_url=effective.get("context_url"),
|
78
|
+
)
|
79
|
+
if args.check:
|
80
|
+
checks = preflight(cfg)
|
81
|
+
print(checks)
|
82
|
+
if not all(checks.values()):
|
83
|
+
return 2
|
84
|
+
if args.dry_run:
|
85
|
+
plan = plan_dry_run(cfg)
|
86
|
+
print(plan)
|
87
|
+
if args.summary_json:
|
88
|
+
import json
|
89
|
+
|
90
|
+
with open(args.summary_json, "w", encoding="utf-8") as f:
|
91
|
+
json.dump(plan, f, indent=2)
|
92
|
+
return 0
|
93
|
+
out = publish(cfg)
|
94
|
+
print(out)
|
95
|
+
if args.summary_json:
|
96
|
+
import json
|
97
|
+
|
98
|
+
with open(args.summary_json, "w", encoding="utf-8") as f:
|
99
|
+
json.dump(out, f, indent=2)
|
100
|
+
return 0
|
101
|
+
|
102
|
+
|
103
|
+
if __name__ == "__main__": # pragma: no cover
|
104
|
+
raise SystemExit(main())
|
@@ -0,0 +1,141 @@
|
|
1
|
+
"""Configuration loading utilities (YAML + env) for Allure hosting publisher.
|
2
|
+
|
3
|
+
Precedence (highest first):
|
4
|
+
1. Explicit CLI / pytest flag values
|
5
|
+
2. Environment variables (ALLURE_BUCKET, ALLURE_PREFIX, ...)
|
6
|
+
3. YAML file values (provided via --config or auto-discovered)
|
7
|
+
4. Built-in defaults
|
8
|
+
|
9
|
+
Auto-discovery order if --config not supplied:
|
10
|
+
- ./allure-host.yml
|
11
|
+
- ./allure-host.yaml
|
12
|
+
- ./.allure-host.yml
|
13
|
+
- ./.allure-host.yaml
|
14
|
+
|
15
|
+
YAML schema (example):
|
16
|
+
|
17
|
+
bucket: my-reports-bucket
|
18
|
+
prefix: reports
|
19
|
+
project: payments
|
20
|
+
branch: main
|
21
|
+
ttl_days: 30
|
22
|
+
max_keep_runs: 20
|
23
|
+
cloudfront: https://reports.example.com
|
24
|
+
retention:
|
25
|
+
default_ttl_days: 30 # alias of ttl_days
|
26
|
+
max_keep_runs: 20 # duplicate path accepted
|
27
|
+
|
28
|
+
Unknown keys are ignored (forward compatible).
|
29
|
+
"""
|
30
|
+
|
31
|
+
from __future__ import annotations
|
32
|
+
|
33
|
+
import os
|
34
|
+
from dataclasses import dataclass
|
35
|
+
from pathlib import Path
|
36
|
+
from typing import Any
|
37
|
+
|
38
|
+
import yaml
|
39
|
+
|
40
|
+
CONFIG_FILENAMES = [
|
41
|
+
"allure-host.yml",
|
42
|
+
"allure-host.yaml",
|
43
|
+
".allure-host.yml",
|
44
|
+
".allure-host.yaml",
|
45
|
+
# Additional generic app config names people often use:
|
46
|
+
"application.yml",
|
47
|
+
"application.yaml",
|
48
|
+
]
|
49
|
+
|
50
|
+
ENV_MAP = {
|
51
|
+
"bucket": "ALLURE_BUCKET",
|
52
|
+
"prefix": "ALLURE_PREFIX",
|
53
|
+
"project": "ALLURE_PROJECT",
|
54
|
+
"branch": "ALLURE_BRANCH",
|
55
|
+
"cloudfront": "ALLURE_CLOUDFRONT",
|
56
|
+
"run_id": "ALLURE_RUN_ID",
|
57
|
+
"ttl_days": "ALLURE_TTL_DAYS",
|
58
|
+
"max_keep_runs": "ALLURE_MAX_KEEP_RUNS",
|
59
|
+
"s3_endpoint": "ALLURE_S3_ENDPOINT",
|
60
|
+
"context_url": "ALLURE_CONTEXT_URL",
|
61
|
+
}
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class LoadedConfig:
|
66
|
+
source_file: Path | None
|
67
|
+
data: dict[str, Any]
|
68
|
+
|
69
|
+
|
70
|
+
def _read_yaml(path: Path) -> dict[str, Any]:
|
71
|
+
try:
|
72
|
+
with path.open("r", encoding="utf-8") as f:
|
73
|
+
content = yaml.safe_load(f) or {}
|
74
|
+
if not isinstance(content, dict):
|
75
|
+
return {}
|
76
|
+
return content
|
77
|
+
except FileNotFoundError:
|
78
|
+
return {}
|
79
|
+
except Exception:
|
80
|
+
# best effort - ignore malformed file
|
81
|
+
return {}
|
82
|
+
|
83
|
+
|
84
|
+
def discover_yaml_config(explicit: str | None = None) -> LoadedConfig:
|
85
|
+
if explicit:
|
86
|
+
p = Path(explicit)
|
87
|
+
return LoadedConfig(
|
88
|
+
source_file=p if p.exists() else None,
|
89
|
+
data=_read_yaml(p),
|
90
|
+
)
|
91
|
+
for name in CONFIG_FILENAMES:
|
92
|
+
p = Path(name)
|
93
|
+
if p.exists():
|
94
|
+
return LoadedConfig(source_file=p, data=_read_yaml(p))
|
95
|
+
return LoadedConfig(source_file=None, data={})
|
96
|
+
|
97
|
+
|
98
|
+
def merge_config(
|
99
|
+
yaml_cfg: dict[str, Any],
|
100
|
+
env: dict[str, str],
|
101
|
+
cli_overrides: dict[str, Any],
|
102
|
+
) -> dict[str, Any]:
|
103
|
+
merged: dict[str, Any] = {}
|
104
|
+
|
105
|
+
# start with YAML
|
106
|
+
merged.update(yaml_cfg)
|
107
|
+
|
108
|
+
# retention nested block normalization
|
109
|
+
retention = yaml_cfg.get("retention")
|
110
|
+
if isinstance(retention, dict):
|
111
|
+
merged.setdefault("ttl_days", retention.get("default_ttl_days"))
|
112
|
+
merged.setdefault("max_keep_runs", retention.get("max_keep_runs"))
|
113
|
+
|
114
|
+
# env overrides
|
115
|
+
for key, env_var in ENV_MAP.items():
|
116
|
+
if env_var in env and env[env_var]:
|
117
|
+
merged[key] = env[env_var]
|
118
|
+
|
119
|
+
# explicit CLI overrides (ignore None only)
|
120
|
+
for k, v in cli_overrides.items():
|
121
|
+
if v is not None:
|
122
|
+
merged[k] = v
|
123
|
+
|
124
|
+
# type adjust
|
125
|
+
for int_field in ("ttl_days", "max_keep_runs"):
|
126
|
+
if int_field in merged and merged[int_field] not in (None, ""):
|
127
|
+
try:
|
128
|
+
merged[int_field] = int(merged[int_field])
|
129
|
+
except ValueError:
|
130
|
+
merged[int_field] = None
|
131
|
+
|
132
|
+
return merged
|
133
|
+
|
134
|
+
|
135
|
+
def load_effective_config(
|
136
|
+
cli_args: dict[str, Any], explicit_config: str | None = None
|
137
|
+
) -> dict[str, Any]:
|
138
|
+
loaded = discover_yaml_config(explicit_config)
|
139
|
+
data = merge_config(loaded.data, os.environ, cli_args)
|
140
|
+
data["_config_file"] = str(loaded.source_file) if loaded.source_file else None
|
141
|
+
return data
|
@@ -0,0 +1,127 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
|
6
|
+
import pytest
|
7
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
8
|
+
|
9
|
+
from .config import load_effective_config
|
10
|
+
from .publisher import plan_dry_run, preflight, publish
|
11
|
+
from .utils import PublishConfig, default_run_id
|
12
|
+
|
13
|
+
|
14
|
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
15
|
+
group = parser.getgroup("allure-host")
|
16
|
+
group.addoption("--allure-bucket", action="store", help="S3 bucket")
|
17
|
+
group.addoption("--allure-prefix", action="store", default="reports")
|
18
|
+
group.addoption("--allure-project", action="store", help="Project name")
|
19
|
+
group.addoption(
|
20
|
+
"--allure-branch",
|
21
|
+
action="store",
|
22
|
+
default=os.getenv("GIT_BRANCH", "main"),
|
23
|
+
)
|
24
|
+
group.addoption(
|
25
|
+
"--allure-cloudfront",
|
26
|
+
action="store",
|
27
|
+
default=os.getenv("ALLURE_CLOUDFRONT"),
|
28
|
+
)
|
29
|
+
group.addoption(
|
30
|
+
"--allure-run-id",
|
31
|
+
action="store",
|
32
|
+
default=os.getenv("ALLURE_RUN_ID", default_run_id()),
|
33
|
+
)
|
34
|
+
group.addoption("--allure-ttl-days", action="store", type=int, default=None)
|
35
|
+
group.addoption("--allure-max-keep-runs", action="store", type=int, default=None)
|
36
|
+
group.addoption("--allure-summary-json", action="store", default=None)
|
37
|
+
group.addoption("--allure-dry-run", action="store_true")
|
38
|
+
group.addoption("--allure-check", action="store_true")
|
39
|
+
group.addoption(
|
40
|
+
"--allure-context-url",
|
41
|
+
action="store",
|
42
|
+
default=os.getenv("ALLURE_CONTEXT_URL"),
|
43
|
+
help="Optional context hyperlink (e.g. Jira ticket)",
|
44
|
+
)
|
45
|
+
group.addoption(
|
46
|
+
"--allure-config",
|
47
|
+
action="store",
|
48
|
+
default=None,
|
49
|
+
help="YAML config file path (optional)",
|
50
|
+
)
|
51
|
+
|
52
|
+
|
53
|
+
def pytest_terminal_summary( # noqa: C901 - central orchestration, readable
|
54
|
+
terminalreporter: pytest.TerminalReporter, exitstatus: int
|
55
|
+
) -> None:
|
56
|
+
config = terminalreporter.config
|
57
|
+
bucket = config.getoption("allure_bucket")
|
58
|
+
project = config.getoption("allure_project")
|
59
|
+
if not bucket or not project:
|
60
|
+
return
|
61
|
+
prefix = config.getoption("allure_prefix")
|
62
|
+
branch = config.getoption("allure_branch")
|
63
|
+
cloudfront = config.getoption("allure_cloudfront")
|
64
|
+
run_id = config.getoption("allure_run_id")
|
65
|
+
ttl_days = config.getoption("allure_ttl_days")
|
66
|
+
max_keep_runs = config.getoption("allure_max_keep_runs")
|
67
|
+
summary_json = config.getoption("allure_summary_json")
|
68
|
+
context_url = config.getoption("allure_context_url")
|
69
|
+
|
70
|
+
cli_overrides = {
|
71
|
+
"bucket": bucket,
|
72
|
+
"prefix": prefix,
|
73
|
+
"project": project,
|
74
|
+
"branch": branch,
|
75
|
+
"cloudfront": cloudfront,
|
76
|
+
"run_id": run_id,
|
77
|
+
"ttl_days": ttl_days,
|
78
|
+
"max_keep_runs": max_keep_runs,
|
79
|
+
"context_url": context_url,
|
80
|
+
}
|
81
|
+
effective = load_effective_config(cli_overrides, config.getoption("allure_config"))
|
82
|
+
# Minimal required
|
83
|
+
if not effective.get("bucket") or not effective.get("project"):
|
84
|
+
return
|
85
|
+
pub_cfg = PublishConfig(
|
86
|
+
bucket=effective["bucket"],
|
87
|
+
prefix=effective.get("prefix") or "reports",
|
88
|
+
project=effective["project"],
|
89
|
+
branch=effective.get("branch") or branch,
|
90
|
+
run_id=effective.get("run_id") or run_id,
|
91
|
+
cloudfront_domain=effective.get("cloudfront"),
|
92
|
+
ttl_days=effective.get("ttl_days"),
|
93
|
+
max_keep_runs=effective.get("max_keep_runs"),
|
94
|
+
context_url=effective.get("context_url"),
|
95
|
+
)
|
96
|
+
|
97
|
+
try:
|
98
|
+
if config.getoption("allure_check"):
|
99
|
+
checks = preflight(pub_cfg)
|
100
|
+
terminalreporter.write_line(f"Allure preflight: {checks}")
|
101
|
+
if summary_json:
|
102
|
+
with open(summary_json, "w", encoding="utf-8") as f:
|
103
|
+
json.dump(checks, f, indent=2)
|
104
|
+
if not all(checks.values()):
|
105
|
+
return
|
106
|
+
if config.getoption("allure_dry_run"):
|
107
|
+
plan = plan_dry_run(pub_cfg)
|
108
|
+
terminalreporter.write_line(f"Allure plan: {plan}")
|
109
|
+
if summary_json:
|
110
|
+
with open(summary_json, "w", encoding="utf-8") as f:
|
111
|
+
json.dump(plan, f, indent=2)
|
112
|
+
return
|
113
|
+
out = publish(pub_cfg)
|
114
|
+
terminalreporter.write_line(
|
115
|
+
f"Allure report published to:\n"
|
116
|
+
f"- run: {out.get('run_url') or pub_cfg.s3_run_prefix}\n"
|
117
|
+
f"- latest: {out.get('latest_url') or pub_cfg.s3_latest_prefix}"
|
118
|
+
)
|
119
|
+
if summary_json:
|
120
|
+
with open(summary_json, "w", encoding="utf-8") as f:
|
121
|
+
json.dump(out, f, indent=2)
|
122
|
+
except (ClientError, BotoCoreError, OSError, ValueError) as e:
|
123
|
+
# Known error categories: AWS client, IO, JSON/value issues
|
124
|
+
terminalreporter.write_line(f"Allure publish failed: {e}")
|
125
|
+
except Exception as e: # noqa: BLE001
|
126
|
+
# Fallback catch-all to prevent pytest terminal crash; log generic
|
127
|
+
terminalreporter.write_line(f"Allure publish failed with unexpected error: {e}")
|