pytest-allure-host 0.1.2__py3-none-any.whl → 2.0.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.
@@ -0,0 +1,158 @@
1
+ """Template and asset constants for Allure hosting publisher.
2
+
3
+ Separating large inline CSS/JS blobs from logic code improves readability and
4
+ keeps `publisher.py` focused on assembling manifests and uploading artifacts.
5
+
6
+ Constants here are intentionally raw (no minification beyond what was already
7
+ present) to avoid altering runtime behaviour. Any future templating engine
8
+ integration can replace these with loader functions while tests assert for
9
+ key sentinel substrings.
10
+ """
11
+
12
+ # flake8: noqa # Long lines expected for embedded assets
13
+
14
+ # ---------------------------- Runs Index CSS ----------------------------
15
+ RUNS_INDEX_CSS_BASE = (
16
+ ":root{--bg:#fff;--bg-alt:#f6f8fa;--border:#d0d7de;--accent:#0366d6;--pass:#2e7d32;--fail:#d32f2f;--broken:#ff9800;--warn:#d18f00;--code-bg:#f2f4f7;--text:#111;--text-dim:#555;}"
17
+ "@media (prefers-color-scheme:dark){:root{--bg:#0d1117;--bg-alt:#161b22;--border:#30363d;--accent:#58a6ff;--text:#e6edf3;--text-dim:#9aa3b1;--code-bg:#1c232b;}}"
18
+ )
19
+
20
+ RUNS_INDEX_CSS_TABLE = (
21
+ "body{font-family:system-ui;margin:1.5rem;background:var(--bg);color:var(--text);}" # noqa: E501
22
+ "table{border-collapse:collapse;width:100%;font-size:13px;}"
23
+ "th,td{padding:.45rem .55rem;border:1px solid var(--border);text-align:left;}"
24
+ "thead th{background:var(--bg-alt);position:sticky;top:0;z-index:2;}"
25
+ "tbody tr:nth-child(even){background:var(--bg-alt);}" # noqa: E501
26
+ "code{background:var(--code-bg);padding:2px 4px;border-radius:3px;font-size:12px;}"
27
+ )
28
+
29
+ RUNS_INDEX_CSS_MISC = (
30
+ ".stats{font-size:12px;color:var(--text-dim);margin:.25rem 0 0;}"
31
+ ".controls{display:flex;flex-wrap:wrap;gap:.5rem;margin:.6rem 0 1rem;align-items:flex-start;}"
32
+ ".controls-section{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center;}"
33
+ ".controls input[type=text]{padding:.4rem .55rem;font-size:13px;border:1px solid var(--border);background:var(--bg-alt);color:var(--text);}" # noqa: E501
34
+ ".badge{display:inline-block;padding:2px 6px;border-radius:999px;font-size:11px;font-weight:600;}"
35
+ ".badge-pass{background:var(--pass);color:#fff;}"
36
+ ".badge-fail{background:var(--fail);color:#fff;}"
37
+ ".badge-broken{background:var(--broken);color:#fff;}"
38
+ "tbody tr.row-fail{outline:2px solid var(--fail);outline-offset:-2px;}"
39
+ ".link-btn.copied{color:var(--pass);}" # existing style piece
40
+ ".col-hidden{display:none !important;}" # hide columns when toggled
41
+ "#col-panel button{font-size:11px;}" # existing style piece
42
+ ".pfb-pass{color:var(--pass);font-weight:600;}"
43
+ ".pfb-fail{color:var(--fail);font-weight:600;}"
44
+ ".pfb-broken{color:var(--broken);font-weight:600;}"
45
+ ".dense td, .dense th{padding:.25rem .35rem !important;font-size:12px;}"
46
+ ".tag-chip{display:inline-block;background:var(--bg-alt);border:1px solid var(--border);padding:2px 5px;margin:0 4px 3px 0;border-radius:12px;font-size:11px;cursor:pointer;user-select:none;}"
47
+ ".tag-chip:hover{background:var(--accent);color:#fff;border-color:var(--accent);}"
48
+ "#spark-wrap{display:flex;flex-direction:column;gap:.25rem;}"
49
+ "#spark{width:140px;height:26px;display:block;}"
50
+ "\n.sr-only{position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden;}"
51
+ )
52
+
53
+ # Additional enhancements CSS (density toggle, in-progress marker, notice)
54
+ RUNS_INDEX_CSS_ENH = (
55
+ "#runs-table.dense td{padding:4px 6px}" # compact row density
56
+ "#runs-table.dense th{padding:4px 6px}" # ensure headers match
57
+ ".run--inprogress .time::after{content:' • running';font-style:italic;color:#666;}"
58
+ "#notice{margin:8px 0;color:#444;font-style:italic;}"
59
+ )
60
+
61
+ # ---------------------------- Runs Index JS ----------------------------
62
+ # NOTE: INIT and BATCH values are injected separately by publisher.
63
+ RUNS_INDEX_JS = (
64
+ "(function(){"
65
+ "const tbl=document.getElementById('runs-table');"
66
+ "const filter=document.getElementById('run-filter');"
67
+ "const stats=document.getElementById('stats');"
68
+ "const pfbStats=document.getElementById('pfb-stats');"
69
+ "const onlyFail=document.getElementById('only-failing');"
70
+ "const clearBtn=document.getElementById('clear-filter');"
71
+ "const themeBtn=document.getElementById('theme-toggle');"
72
+ "const accentBtn=document.getElementById('accent-toggle');"
73
+ "const colBtn=document.getElementById('col-toggle');"
74
+ "const densityBtn=document.getElementById('density-toggle');"
75
+ "const tzBtn=document.getElementById('tz-toggle');"
76
+ "let localTime=false;"
77
+ "let colPanel=null;"
78
+ "const LS='ah_runs_';"
79
+ "function lsGet(k){try{return localStorage.getItem(LS+k);}catch(e){return null;}}"
80
+ "function lsSet(k,v){try{localStorage.setItem(LS+k,v);}catch(e){}}"
81
+ "function hidden(){return [...tbl.tBodies[0].querySelectorAll('tr.pr-hidden')];}"
82
+ "function updateLoadButton(){const hiddenRows=hidden();const loadBtn=document.getElementById('load-more');if(!loadBtn)return;if(hiddenRows.length){loadBtn.style.display='inline-block';loadBtn.textContent='Load more ('+hiddenRows.length+')';}else{loadBtn.style.display='none';}}"
83
+ "function revealNextBatch(batch){hidden().slice(0,batch).forEach(r=>r.classList.remove('pr-hidden'));updateLoadButton();}" # progressive reveal
84
+ "function failingTotal(){return [...tbl.tBodies[0].rows].reduce((a,r)=>a+ (Number(r.dataset.failed||0)>0?1:0),0);}"
85
+ "function applyStats(){const total=tbl.tBodies[0].rows.length;const rows=[...tbl.tBodies[0].rows];const vis=rows.filter(r=>r.style.display!=='none');stats.textContent=vis.length+' / '+total+' shown';let p=0,f=0,b=0;vis.forEach(r=>{p+=Number(r.dataset.passed||0);f+=Number(r.dataset.failed||0);b+=Number(r.dataset.broken||0);});pfbStats.textContent=' P:'+p+' F:'+f+' B:'+b;}"
86
+ "function applyFooter(){const total=tbl.tBodies[0].rows.length;const hid=hidden().length;const el=document.getElementById('footer-stats');if(el){el.textContent=(total-hid)+' / '+total+' loaded';}}"
87
+ "function applyFilter(){const loadBtn=document.getElementById('load-more');const raw=filter.value.trim().toLowerCase();const tokens=raw.split(/\\s+/).filter(Boolean);const onlyF=onlyFail.checked;if(tokens.length&&document.querySelector('.pr-hidden')){hidden().forEach(r=>r.classList.remove('pr-hidden'));updateLoadButton();}const rows=[...tbl.tBodies[0].rows];rows.forEach(r=>{const hay=r.getAttribute('data-search')||'';const hasTxt=!tokens.length||tokens.every(t=>hay.indexOf(t)>-1);const failing=Number(r.dataset.failed||0)>0;r.style.display=(hasTxt&&(!onlyF||failing))?'':'none';if(failing){r.classList.add('failing-row');}else{r.classList.remove('failing-row');}});document.querySelectorAll('tr.row-active').forEach(x=>x.classList.remove('row-active'));if(tokens.length===1){const rid=tokens[0];const match=[...tbl.tBodies[0].rows].find(r=>r.querySelector('td.col-run_id code')&&r.querySelector('td.col-run_id code').textContent.trim().toLowerCase()===rid);if(match)match.classList.add('row-active');}applyStats();}"
88
+ "function relFmt(sec){if(sec<60)return Math.floor(sec)+'s';sec/=60;if(sec<60)return Math.floor(sec)+'m';sec/=60;if(sec<24)return Math.floor(sec)+'h';sec/=24;if(sec<7)return Math.floor(sec)+'d';const w=Math.floor(sec/7);if(w<4)return w+'w';const mo=Math.floor(sec/30);if(mo<12)return mo+'mo';return Math.floor(sec/365)+'y';}"
89
+ "function updateAges(){const now=Date.now()/1000;tbl.tBodies[0].querySelectorAll('td.age').forEach(td=>{const ep=Number(td.getAttribute('data-epoch'));if(!ep){td.textContent='-';return;}td.textContent=relFmt(now-ep);});}"
90
+ "const loadBtn=document.getElementById('load-more');if(loadBtn){loadBtn.addEventListener('click',()=>{revealNextBatch(Number(loadBtn.getAttribute('data-batch'))||300);applyFilter();lsSet('loaded',String(tbl.tBodies[0].rows.length-hidden().length));});}"
91
+ "// Infinite scroll (observer)"
92
+ "if('IntersectionObserver' in window && loadBtn){const io=new IntersectionObserver(es=>{es.forEach(e=>{if(e.isIntersecting && hidden().length){revealNextBatch(Number(loadBtn.getAttribute('data-batch'))||300);applyFilter();}});},{root:null,rootMargin:'120px'});io.observe(loadBtn);}"
93
+ "filter.addEventListener('input',()=>{applyFilter();lsSet('filter',filter.value);});"
94
+ "filter.addEventListener('keydown',e=>{if(e.key==='Enter'){applyFilter();}});"
95
+ "onlyFail.addEventListener('change',()=>{applyFilter();lsSet('onlyFail',onlyFail.checked?'1':'0');});"
96
+ "clearBtn&&clearBtn.addEventListener('click',()=>{filter.value='';onlyFail.checked=false;applyFilter();filter.focus();});"
97
+ "const ACCENTS=['#0366d6','#6f42c1','#d32f2f','#1b7f3b','#b08800'];"
98
+ "function applyAccent(c){document.documentElement.style.setProperty('--accent',c);lsSet('accent',c);}" # noqa: E501
99
+ "accentBtn&&accentBtn.addEventListener('click',()=>{const cur=lsGet('accent')||ACCENTS[0];const idx=(ACCENTS.indexOf(cur)+1)%ACCENTS.length;applyAccent(ACCENTS[idx]);});"
100
+ "function applyTheme(mode){document.documentElement.classList.remove('force-light','force-dark');if(mode==='light'){document.documentElement.classList.add('force-light');}else if(mode==='dark'){document.documentElement.classList.add('force-dark');}lsSet('theme',mode);}" # noqa: E501
101
+ "themeBtn&&themeBtn.addEventListener('click',()=>{const cur=lsGet('theme')||'auto';const next=cur==='auto'?'dark':(cur==='dark'?'light':'auto');applyTheme(next);themeBtn.textContent='Theme('+next+')';});"
102
+ "densityBtn&&densityBtn.addEventListener('click',()=>{const dense=tbl.classList.toggle('dense');lsSet('dense',dense?'1':'0');densityBtn.textContent=dense?'Dense(-)':'Dense(+)' ;});"
103
+ "tzBtn&&tzBtn.addEventListener('click',()=>{localTime=!localTime;updateTimeCells();tzBtn.textContent=localTime?'UTC':'Local';lsSet('tz',localTime?'local':'utc');});"
104
+ "function updateTimeCells(){[...tbl.tBodies[0].rows].forEach(r=>{const utcCell=r.querySelector('td.col-utc');if(!utcCell)return;const ep=Number(r.dataset.epoch||0);if(!ep){utcCell.textContent='-';return;}if(localTime){const d=new Date(ep*1000);utcCell.textContent=d.toLocaleString();}else{const d=new Date(ep*1000);utcCell.textContent=d.toISOString().replace('T',' ').slice(0,19);} });}"
105
+ "function extract(r,col){if(col.startsWith('meta:')){const idx=[...tbl.tHead.querySelectorAll('th')].findIndex(h=>h.dataset.col===col);return idx>-1?r.cells[idx].textContent:'';}switch(col){case 'size':return r.querySelector('td.col-size').getAttribute('title');case 'files':return r.querySelector('td.col-files').getAttribute('title');case 'pfb':return r.querySelector('td.col-pfb').textContent;case 'passpct':return r.querySelector('td.col-passpct').textContent;case 'run_id':return r.querySelector('td.col-run_id').textContent;case 'utc':return r.querySelector('td.col-utc').textContent;case 'context':return r.querySelector('td.col-context').textContent;case 'tags':return r.querySelector('td.col-tags').textContent;default:return r.textContent;}}"
106
+ "let sortState=null;"
107
+ "function sortBy(th){const col=th.dataset.col;const tbody=tbl.tBodies[0];const rows=[...tbody.rows];let dir=1;if(sortState&&sortState.col===col){dir=-sortState.dir;}sortState={col,dir};const numeric=(col==='size'||col==='files');rows.sort((r1,r2)=>{const a=extract(r1,col);const b=extract(r2,col);if(numeric){return ((Number(a)||0)-(Number(b)||0))*dir;}return a.localeCompare(b)*dir;});rows.forEach(r=>tbody.appendChild(r));tbl.tHead.querySelectorAll('th.sortable').forEach(h=>h.removeAttribute('data-sort'));th.setAttribute('data-sort',dir===1?'asc':'desc');updateAriaSort();lsSet('sort_col',col);lsSet('sort_dir',String(dir));}"
108
+ "tbl.tHead.querySelectorAll('th.sortable').forEach(th=>{th.addEventListener('click',()=>sortBy(th));});"
109
+ "function updateAriaSort(){tbl.tHead.querySelectorAll('th.sortable').forEach(th=>{th.setAttribute('aria-sort','none');});if(sortState){const th=tbl.tHead.querySelector(`th[data-col='${sortState.col}']`);if(th)th.setAttribute('aria-sort',sortState.dir===1?'ascending':'descending');}}" # noqa: E501
110
+ )
111
+ # Ensure sortBy calls updateAriaSort: simple append inside closure
112
+ if "function sortBy(th)" in RUNS_INDEX_JS:
113
+ RUNS_INDEX_JS = RUNS_INDEX_JS.replace(
114
+ "rows.forEach(r=>tbody.appendChild(r));tbl.tHead.querySelectorAll('th.sortable').forEach(h=>h.removeAttribute('data-sort'));th.setAttribute('data-sort',dir===1?'asc':'desc');lsSet('sort_col',col);lsSet('sort_dir',String(dir));}",
115
+ "rows.forEach(r=>tbody.appendChild(r));tbl.tHead.querySelectorAll('th.sortable').forEach(h=>h.removeAttribute('data-sort'));th.setAttribute('data-sort',dir===1?'asc':'desc');updateAriaSort();lsSet('sort_col',col);lsSet('sort_dir',String(dir));}",
116
+ )
117
+
118
+ # Sentinel substrings used in tests to verify template inclusion
119
+ RUNS_INDEX_SENTINELS = [
120
+ "ah_runs_",
121
+ "col-toggle",
122
+ "function applyFilter()",
123
+ ]
124
+
125
+ # Post-bootstrap JS enhancements: aria-sort hook exposure, filter+URL glue,
126
+ # density persistence (already partly handled), failing-run notice, and
127
+ # in-progress marking using data-end-iso absence (v1 contract compliance).
128
+ RUNS_INDEX_JS_ENH = (
129
+ "document.addEventListener('DOMContentLoaded',()=>{"
130
+ "const table=document.getElementById('runs-table');if(!table)return;"
131
+ "if(localStorage.getItem('runs.dense')==='1'){table.classList.add('dense');}"
132
+ "table.querySelectorAll('tbody tr[data-v=\"1\"]').forEach(tr=>{if(!tr.dataset.endIso){tr.classList.add('run--inprogress');}});"
133
+ "window.setAriaSort=function(idx,dir){table.querySelectorAll('thead th').forEach((th,i)=>th.setAttribute('aria-sort',i===idx?dir:'none'));};"
134
+ "function setQS(k,v){const q=new URLSearchParams(location.search);if(v){q.set(k,v);}else{q.delete(k);}history.replaceState(null,'','?'+q);}"
135
+ "function applyFilters(){const q=new URLSearchParams(location.search);const gi=id=>document.getElementById(id);const branch=(gi('f-branch')?gi('f-branch').value.trim():q.get('branch')||'');const tagsStr=(gi('f-tags')?gi('f-tags').value.trim():q.get('tags')||'');const tags=tagsStr.split(',').filter(Boolean);const from=(gi('f-from')?gi('f-from').value:q.get('from'));const to=(gi('f-to')?gi('f-to').value:q.get('to'));let failing=(gi('f-onlyFailing')?gi('f-onlyFailing').checked:(q.get('onlyFailing')==='1')),anyFail=false;const fromEpoch=from?Date.parse(from+'T00:00:00Z')/1000:0;const toEpoch=to?(Date.parse(to+'T23:59:59Z')/1000):0;table.querySelectorAll('tbody tr[data-v=\"1\"]').forEach(tr=>{const rowBr=(tr.dataset.branch||'');const okBranch=!branch||rowBr===branch||rowBr.indexOf(branch)>=0;let rowTags=[];try{rowTags=JSON.parse(tr.dataset.tags||'[]');}catch(e){}const okTags=!tags.length||tags.every(t=>rowTags.includes(t));const epoch=parseInt(tr.dataset.epoch||'0',10);const okFrom=!from|| (epoch && epoch>=fromEpoch);const okTo=!to|| (epoch && epoch<=toEpoch);const isFail=(parseInt(tr.dataset.f||'0',10)>0);if(isFail)anyFail=true;const okFail=!failing||isFail;tr.hidden=!(okBranch&&okTags&&okFrom&&okTo&&okFail);});if(failing&&!anyFail){const q2=new URLSearchParams(location.search);q2.delete('onlyFailing');history.replaceState(null,'','?'+q2);let n=document.getElementById('notice');if(!n){n=document.createElement('div');n.id='notice';document.body.insertBefore(n,document.body.firstChild);}n.setAttribute('role','status');n.textContent='No failing runs — filter cleared.';}}"
136
+ "window.applyFilters=applyFilters;applyFilters();window._setQSFilter=setQS;"
137
+ "});"
138
+ )
139
+
140
+ # ---------------------------- Dashboard / Summary JS & CSS ----------------------------
141
+ # Consolidated summary cards + empty state + toggle logic, extracted from inline string.
142
+ RUNS_INDEX_DASHBOARD_CSS = (
143
+ ".sc-toggle{margin:.25rem 0;padding:.25rem .5rem;border:1px solid var(--border,#2b2b2b);background:var(--bg-alt);border-radius:.5rem;cursor:pointer;font-size:12px}"
144
+ "#summary-cards .v.ok{color:#0a7a0a}#summary-cards .v.warn{color:#b8860b}#summary-cards .v.bad{color:#b00020}"
145
+ ".empty{margin:.5rem 0;padding:.6rem .8rem;border:1px solid var(--border,#2b2b2b);border-radius:.5rem;background:var(--bg-alt);opacity:.85;font-size:12px}"
146
+ )
147
+
148
+ RUNS_INDEX_DASHBOARD_JS = (
149
+ "(function(){const tbl=document.getElementById('runs-table');const cards=document.getElementById('summary-cards');if(!tbl||!cards)return;"
150
+ "if(!document.getElementById('sc-span')){const spanCard=document.createElement('div');spanCard.className='card';spanCard.innerHTML='<div class=\"k\">Time span</div><div class=\"v\" id=\"sc-span\">—</div>';cards.appendChild(spanCard);}"
151
+ "const empty=document.getElementById('empty-msg');const toggle=document.getElementById('summary-toggle');"
152
+ "function visibleRows(){return [...tbl.querySelectorAll('tbody tr[data-v]')].filter(r=>!(r.hidden||getComputedStyle(r).display==='none'));}"
153
+ "function fmtPct(n){return isFinite(n)?(Math.round(n*10)/10).toFixed(1)+'%':'—';}"
154
+ r"function update(){const rows=visibleRows();if(empty)empty.hidden=rows.length!==0;const passEl=document.getElementById('sc-pass');const failEl=document.getElementById('sc-fail');const countEl=document.getElementById('sc-count');const latestEl=document.getElementById('sc-latest');let p=0,f=0,b=0,latestIso=null,latestId='—';rows.forEach(r=>{p+=+r.dataset.p||0;f+=+r.dataset.f||0;b+=+r.dataset.b||0;const iso=r.dataset.startIso||r.querySelector('[data-iso]')?.getAttribute('data-iso');if(iso&&(!latestIso||iso>latestIso)){latestIso=iso;latestId=r.dataset.runId||r.getAttribute('data-run-id')||'—';}});if(rows.length===0){[passEl,failEl,countEl,latestEl].forEach(el=>el&&(el.textContent='—'));const span=document.getElementById('sc-span');if(span)span.textContent='—';return;}const total=p+f+b;passEl&&(passEl.textContent=total?fmtPct(p/total*100):'—',passEl.classList.remove('ok','warn','bad'),(()=>{const num=parseFloat(passEl.textContent)||NaN;if(!isNaN(num))passEl.classList.add(num>=90?'ok':(num>=75?'warn':'bad'));})());failEl&&(failEl.textContent=String(f));countEl&&(countEl.textContent=String(rows.length));if(latestEl){if(latestIso){const base=location.pathname.replace(/runs/index\.html.*/,'');latestEl.innerHTML='<a href=\"'+base+latestId+'/\" title=\"Open latest run\">'+latestId+'</a>'; }else{latestEl.textContent=latestId;}}const span=document.getElementById('sc-span');if(span){const isoVals=rows.map(r=>r.dataset.startIso||'').filter(Boolean).sort();if(isoVals.length){const fmt=iso=>{try{return new Date(iso).toLocaleString(undefined,{dateStyle:'medium',timeStyle:'short'});}catch(e){return iso;}};span.textContent=fmt(isoVals[0])+' → '+fmt(isoVals[isoVals.length-1]);}}}"
155
+ "const orig=window.applyFilters;window.applyFilters=function(){orig&&orig();update();};document.addEventListener('DOMContentLoaded',update);update();"
156
+ "if(toggle){const key='runs.summary.collapsed';function setC(c){cards.hidden=c;toggle.setAttribute('aria-expanded',String(!c));toggle.textContent=c?'Summary ▶':'Summary ▼';try{localStorage.setItem(key,c?'1':'0');}catch(e){}}toggle.addEventListener('click',()=>setC(!cards.hidden));setC((()=>{try{return localStorage.getItem(key)==='1';}catch(e){return false;}})());}"
157
+ "})();"
158
+ )
@@ -31,6 +31,10 @@ def cache_control_for_key(key: str) -> str:
31
31
  # widgets optional no-cache could be configurable later
32
32
  if "/widgets/" in key:
33
33
  return "no-cache"
34
+ # History JSON (e.g., latest/history/history-trend.json) changes frequently
35
+ # and should not be cached aggressively to ensure trend views refresh.
36
+ if "/history/" in key and key.endswith(".json"):
37
+ return "no-cache"
34
38
  return "public, max-age=31536000, immutable"
35
39
 
36
40
 
@@ -58,6 +62,15 @@ class PublishConfig:
58
62
  sse_kms_key_id: str | None = None
59
63
  # arbitrary metadata (jira ticket, environment, etc.)
60
64
  metadata: dict | None = None
65
+ # performance tuning
66
+ upload_workers: int | None = None # parallel upload threads
67
+ copy_workers: int | None = None # parallel copy threads
68
+ # optional archive artifact (compressed run bundle)
69
+ archive_run: bool | None = None
70
+ archive_format: str | None = None # 'tar.gz' or 'zip'
71
+ # UI feature toggles
72
+ # allow disabling summary cards dashboard if desired
73
+ summary_cards: bool = True
61
74
 
62
75
  @property
63
76
  def s3_run_prefix(self) -> str:
@@ -79,6 +92,17 @@ class PublishConfig:
79
92
  )
80
93
  return keys["latest"]
81
94
 
95
+ @property
96
+ def s3_latest_prefix_tmp(self) -> str:
97
+ """Temporary staging prefix used during two-phase latest promotion.
98
+
99
+ Mirrors publish(): objects are first copied into latest_tmp/ then a
100
+ delete + copy sequence promotes them into latest/. Exposed so
101
+ plan_dry_run can surface this path for CI planning & tests.
102
+ """
103
+ root = f"{self.prefix.rstrip('/')}/{self.project}/{self.branch}"
104
+ return f"{root}/latest_tmp/"
105
+
82
106
  def url_run(self) -> str | None:
83
107
  if not self.cloudfront_domain:
84
108
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-allure-host
3
- Version: 0.1.2
3
+ Version: 2.0.1
4
4
  Summary: Publish Allure static reports to private S3 behind CloudFront with history preservation
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -17,6 +17,7 @@ Classifier: Intended Audience :: Developers
17
17
  Classifier: Topic :: Software Development :: Testing
18
18
  Classifier: Framework :: Pytest
19
19
  Classifier: Development Status :: 3 - Alpha
20
+ Classifier: License :: OSI Approved :: MIT License
20
21
  Classifier: Operating System :: OS Independent
21
22
  Requires-Dist: PyYAML (>=6,<7)
22
23
  Requires-Dist: boto3 (>=1.28,<2.0)
@@ -34,9 +35,12 @@ Description-Content-Type: text/markdown
34
35
  ![PyPI - Version](https://img.shields.io/pypi/v/pytest-allure-host.svg)
35
36
  ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)
36
37
  [![Docs](https://img.shields.io/badge/docs-site-blue)](https://darrenrabbs.github.io/allurehosting/)
38
+ [![CDK Stack](https://img.shields.io/badge/CDK%20Stack-repo-blueviolet)](https://github.com/darrenrabbs/allurehosting-cdk)
37
39
 
38
40
  Publish Allure static reports to private S3 behind CloudFront with history preservation and SPA-friendly routing.
39
41
 
42
+ Optional infrastructure (AWS CDK stack to provision the private S3 bucket + CloudFront OAC distribution) lives externally: https://github.com/darrenrabbs/allurehosting-cdk
43
+
40
44
  See `docs/architecture.md` and `.github/copilot-instructions.md` for architecture and design constraints.
41
45
 
42
46
  ## Documentation
@@ -63,6 +67,39 @@ The README intentionally stays lean—refer to the site for detailed guidance.
63
67
  - Columns: Run ID, raw epoch, UTC Time (human readable), Size (pretty units), P/F/B (passed/failed/broken counts), links to the immutable run and the moving latest
64
68
  - Newest run highlighted with a star (★) and soft background
65
69
 
70
+ ## Quick start
71
+
72
+ ```bash
73
+ # Install the publisher
74
+ pip install pytest-allure-host
75
+
76
+ # Run your test suite and produce allure-results/
77
+ pytest --alluredir=allure-results
78
+
79
+ # Plan (no uploads) – shows what would be published
80
+ publish-allure \
81
+ --bucket my-allure-bucket \
82
+ --project myproj \
83
+ --branch main \
84
+ --dry-run --summary-json plan.json
85
+
86
+ # Real publish (requires AWS creds: env vars, profile, or OIDC)
87
+ publish-allure \
88
+ --bucket my-allure-bucket \
89
+ --project myproj \
90
+ --branch main
91
+ ```
92
+
93
+ Notes:
94
+
95
+ - `--prefix` defaults to `reports`; omit unless you need a different root.
96
+ - `--branch` defaults to `$GIT_BRANCH` or `main` if unset.
97
+ - Add `--cloudfront https://reports.example.com` to print CDN URLs.
98
+ - Use `--check` to preflight (AWS / allure binary / inputs) before a real run.
99
+ - Add `--context-url https://jira.example.com/browse/PROJ-123` to link a change ticket in the runs index.
100
+ - Use `--dry-run` + `--summary-json` in CI for a planning stage artifact.
101
+ - Provide `--ttl-days` and/or `--max-keep-runs` for lifecycle & cost controls.
102
+
66
103
  ## Requirements
67
104
 
68
105
  - Python 3.9+
@@ -230,6 +267,56 @@ Pytest-driven (plugin):
230
267
  --allure-max-keep-runs 10
231
268
  ```
232
269
 
270
+ ### Minimal publish-only workflow
271
+
272
+ Create `.github/workflows/allure-publish.yml` for a lightweight pipeline that runs tests, generates the report, and publishes it (using secrets for the bucket and AWS credentials):
273
+
274
+ ```yaml
275
+ name: allure-publish
276
+ on: [push, pull_request]
277
+ jobs:
278
+ publish:
279
+ runs-on: ubuntu-latest
280
+ permissions:
281
+ contents: read
282
+ steps:
283
+ - uses: actions/checkout@v4
284
+ - uses: actions/setup-python@v5
285
+ with:
286
+ python-version: "3.11"
287
+ - name: Install deps (minimal)
288
+ run: pip install pytest pytest-allure-host allure-pytest
289
+ - name: Run tests
290
+ run: pytest --alluredir=allure-results -q
291
+ - name: Publish Allure report (dry-run on PRs)
292
+ env:
293
+ AWS_REGION: us-east-1
294
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
295
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
296
+ ALLURE_BUCKET: ${{ secrets.ALLURE_BUCKET }}
297
+ run: |
298
+ EXTRA=""
299
+ if [ "${{ github.event_name }}" = "pull_request" ]; then EXTRA="--dry-run"; fi
300
+ publish-allure \
301
+ --bucket "$ALLURE_BUCKET" \
302
+ --project myproj \
303
+ --branch "${{ github.ref_name }}" \
304
+ --summary-json summary.json $EXTRA
305
+ - name: Upload publish summary (always)
306
+ if: always()
307
+ uses: actions/upload-artifact@v4
308
+ with:
309
+ name: allure-summary
310
+ path: summary.json
311
+ ```
312
+
313
+ Notes:
314
+
315
+ - Add `--cloudfront https://reports.example.com` if you have a CDN domain.
316
+ - Add `--context-url ${{ github.server_url }}/${{ github.repository }}/pull/${{ github.event.pull_request.number }}` inside PRs to link the run to its PR.
317
+ - Use `--max-keep-runs` / `--ttl-days` to manage storage costs.
318
+ - For LocalStack-based tests, set `--s3-endpoint` and export `ALLURE_S3_ENDPOINT` in `env:`.
319
+
233
320
  ## Troubleshooting
234
321
 
235
322
  - Missing Allure binary: ensure the Allure CLI is installed and on PATH.
@@ -0,0 +1,13 @@
1
+ pytest_allure_host/__init__.py,sha256=v0peulTWwWlsJ0TpKgrjQx79plmqPp3pCOJyWJHAJPk,366
2
+ pytest_allure_host/__main__.py,sha256=1dzo3_74YYjXLo4xf_OjbmayOAzDdTlIgCVX00tHdoU,99
3
+ pytest_allure_host/cli.py,sha256=43wnhhcerl20fJcfQbtWIuR7CDHI--V1oIVpwaoV_PI,10235
4
+ pytest_allure_host/config.py,sha256=QLWhSYemmyI7cu_Y9ToR51hHwdU2lLLGqYT6IG9d1OE,4827
5
+ pytest_allure_host/plugin.py,sha256=t_DzPQAf3Vj8W9Xmt25__iZg7ItRdJzl7tyQu01ZtSI,5079
6
+ pytest_allure_host/publisher.py,sha256=NCz8FHA7rRes4vwIfJbjfuipMmdj1MxMicYZoXkMSfc,94845
7
+ pytest_allure_host/templates.py,sha256=tewywUUzhoLNayukhw-tOrSGtacMYSojzBKxE-cfRJE,18960
8
+ pytest_allure_host/utils.py,sha256=rRgvPwfHX2cviczjTkPpQ1SDpsXS8xLvQaVVAdZegzo,4449
9
+ pytest_allure_host-2.0.1.dist-info/METADATA,sha256=jMAFcp6u0gNz4qfl3Tx-LtCITSmGrvfwZ5KNyDZL9-A,13621
10
+ pytest_allure_host-2.0.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
11
+ pytest_allure_host-2.0.1.dist-info/entry_points.txt,sha256=PWnSY4aqAumEX4-dc1TkNHfju5VTKPUHfYPv1SdAlIA,119
12
+ pytest_allure_host-2.0.1.dist-info/licenses/LICENSE,sha256=jcHN7r3njxuM4ilSLkK0fuuQAGMMkqzvYL27CYZGnWs,1084
13
+ pytest_allure_host-2.0.1.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- pytest_allure_host/__init__.py,sha256=Us3L46Kd1Rh1FM9e74PJB7TN4G37WkM12edi8GnGhgc,130
2
- pytest_allure_host/__main__.py,sha256=1dzo3_74YYjXLo4xf_OjbmayOAzDdTlIgCVX00tHdoU,99
3
- pytest_allure_host/cli.py,sha256=OLizf1zTZ4yeppg1dcv9QYqJBwoa1FsXUoBgJAmd_o8,4932
4
- pytest_allure_host/config.py,sha256=QLWhSYemmyI7cu_Y9ToR51hHwdU2lLLGqYT6IG9d1OE,4827
5
- pytest_allure_host/plugin.py,sha256=t_DzPQAf3Vj8W9Xmt25__iZg7ItRdJzl7tyQu01ZtSI,5079
6
- pytest_allure_host/publisher.py,sha256=0qTaJW5gFIbjM26uyn5b49gqqTJWbxgWvCt7PCnhQfA,32691
7
- pytest_allure_host/utils.py,sha256=oZm3RqhTYMsyf-3IN4ugFLK3gEz9jQ_rmdb2BeQoYBU,3329
8
- pytest_allure_host-0.1.2.dist-info/METADATA,sha256=MyVVqcfrynwG4j4YkztkGRNBRFgq_PNPGGSxF5FJSj8,10387
9
- pytest_allure_host-0.1.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
10
- pytest_allure_host-0.1.2.dist-info/entry_points.txt,sha256=PWnSY4aqAumEX4-dc1TkNHfju5VTKPUHfYPv1SdAlIA,119
11
- pytest_allure_host-0.1.2.dist-info/licenses/LICENSE,sha256=jcHN7r3njxuM4ilSLkK0fuuQAGMMkqzvYL27CYZGnWs,1084
12
- pytest_allure_host-0.1.2.dist-info/RECORD,,