Skip to content

Commit 2c3709d

Browse files
feat: Centralize schema by migrating base models, types, and unit system to flow360-schema
Migrate Flow360BaseModel, length types, and operation condition models to the external flow360-schema package. Remove now-redundant mixin classes and update dependencies. Add CodeArtifact CI setup and coverage reporting.
1 parent 8e17457 commit 2c3709d

File tree

61 files changed

+978
-1096
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+978
-1096
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Setup CodeArtifact Poetry Auth
2+
description: Configure AWS credentials, fetch CodeArtifact token, and set Poetry auth env vars.
3+
4+
inputs:
5+
aws-access-key-id:
6+
description: AWS access key id for CodeArtifact access.
7+
required: true
8+
aws-secret-access-key:
9+
description: AWS secret access key for CodeArtifact access.
10+
required: true
11+
aws-region:
12+
description: AWS region of the CodeArtifact domain.
13+
required: false
14+
default: us-east-1
15+
domain:
16+
description: CodeArtifact domain name.
17+
required: false
18+
default: flexcompute
19+
domain-owner:
20+
description: AWS account id that owns the CodeArtifact domain.
21+
required: false
22+
default: "625554095313"
23+
24+
outputs:
25+
token:
26+
description: CodeArtifact authorization token.
27+
value: ${{ steps.get-token.outputs.token }}
28+
29+
runs:
30+
using: composite
31+
steps:
32+
- name: Configure AWS credentials
33+
uses: aws-actions/configure-aws-credentials@v4
34+
with:
35+
aws-access-key-id: ${{ inputs.aws-access-key-id }}
36+
aws-secret-access-key: ${{ inputs.aws-secret-access-key }}
37+
aws-region: ${{ inputs.aws-region }}
38+
39+
- name: Get CodeArtifact token and set Poetry env
40+
id: get-token
41+
shell: bash
42+
run: |
43+
TOKEN=$(aws codeartifact get-authorization-token \
44+
--domain "${{ inputs.domain }}" \
45+
--domain-owner "${{ inputs.domain-owner }}" \
46+
--region "${{ inputs.aws-region }}" \
47+
--query authorizationToken \
48+
--output text)
49+
echo "::add-mask::$TOKEN"
50+
echo "token=$TOKEN" >> "$GITHUB_OUTPUT"
51+
echo "POETRY_HTTP_BASIC_CODEARTIFACT_USERNAME=aws" >> "$GITHUB_ENV"
52+
echo "POETRY_HTTP_BASIC_CODEARTIFACT_PASSWORD=$TOKEN" >> "$GITHUB_ENV"
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#!/usr/bin/env python3
2+
"""Generate a coverage summary from Cobertura XML, with optional diff coverage."""
3+
4+
import os
5+
import re
6+
import subprocess
7+
import sys
8+
import xml.etree.ElementTree as ET
9+
from collections import defaultdict
10+
11+
12+
def make_bar(pct, width=20):
13+
pct = max(0.0, min(100.0, pct))
14+
filled = round(pct / 100 * width)
15+
return "\u2593" * filled + "\u2591" * (width - filled)
16+
17+
18+
def status_icon(pct):
19+
if pct >= 80:
20+
return "\U0001f7e2"
21+
if pct >= 60:
22+
return "\U0001f7e1"
23+
return "\U0001f534"
24+
25+
26+
def _get_repo_root():
27+
result = subprocess.run(
28+
["git", "rev-parse", "--show-toplevel"],
29+
capture_output=True,
30+
text=True,
31+
check=True,
32+
)
33+
return result.stdout.strip()
34+
35+
36+
_repo_root = None
37+
38+
39+
def _normalize_filename(filename, source_roots):
40+
"""Normalize coverage XML filename to repo-relative path.
41+
42+
Coverage XML records paths relative to <source> roots, while git diff
43+
produces paths relative to the repo root. This joins the two and strips
44+
the repo root prefix so both sides use the same reference frame.
45+
"""
46+
global _repo_root
47+
if _repo_root is None:
48+
_repo_root = _get_repo_root()
49+
repo_prefix = _repo_root + "/"
50+
51+
for root in source_roots:
52+
if os.path.isabs(filename):
53+
if filename.startswith(repo_prefix):
54+
return filename[len(repo_prefix) :]
55+
else:
56+
full = os.path.join(root, filename)
57+
if full.startswith(repo_prefix):
58+
return full[len(repo_prefix) :]
59+
return filename
60+
61+
62+
def parse_coverage_xml(xml_path, depth):
63+
"""Parse coverage.xml, return (groups_dict, file_line_coverage_dict)."""
64+
tree = ET.parse(xml_path)
65+
root = tree.getroot()
66+
67+
source_roots = [s.text.rstrip("/") for s in root.findall(".//source") if s.text]
68+
69+
groups = defaultdict(lambda: {"hits": 0, "lines": 0})
70+
file_coverage = defaultdict(dict)
71+
72+
for pkg in root.findall(".//package"):
73+
name = pkg.get("name", "")
74+
parts = name.split(".")
75+
key = ".".join(parts[:depth]) if len(parts) >= depth else name
76+
77+
for cls in pkg.findall(".//class"):
78+
filename = cls.get("filename", "")
79+
filename = _normalize_filename(filename, source_roots)
80+
for line in cls.findall(".//line"):
81+
line_num = int(line.get("number", "0"))
82+
hit = int(line.get("hits", "0")) > 0
83+
file_coverage[filename][line_num] = (
84+
file_coverage[filename].get(line_num, False) or hit
85+
)
86+
groups[key]["lines"] += 1
87+
if hit:
88+
groups[key]["hits"] += 1
89+
90+
return groups, file_coverage
91+
92+
93+
def get_changed_lines(diff_branch):
94+
"""Run git diff and return {filepath: set_of_changed_line_numbers} for non-test .py files."""
95+
result = subprocess.run(
96+
["git", "diff", "--unified=0", diff_branch, "--", "*.py"],
97+
capture_output=True,
98+
text=True,
99+
check=True,
100+
)
101+
102+
changed = defaultdict(set)
103+
current_file = None
104+
hunk_re = re.compile(r"^@@ .+?\+(\d+)(?:,(\d+))? @@")
105+
106+
for line in result.stdout.splitlines():
107+
if line.startswith("+++ b/"):
108+
current_file = line[6:]
109+
elif line.startswith("@@") and current_file:
110+
m = hunk_re.match(line)
111+
if m:
112+
start = int(m.group(1))
113+
count = int(m.group(2)) if m.group(2) else 1
114+
if count > 0:
115+
for i in range(start, start + count):
116+
changed[current_file].add(i)
117+
118+
return {
119+
f: lines
120+
for f, lines in changed.items()
121+
if f.endswith(".py") and not f.startswith("tests/") and f.startswith("flow360/")
122+
}
123+
124+
125+
def build_diff_coverage_md(changed_lines, file_coverage):
126+
"""Build diff coverage markdown section."""
127+
if not changed_lines:
128+
return "## Diff Coverage\n\nNo implementation files changed.\n"
129+
130+
total_covered = 0
131+
total_changed = 0
132+
133+
file_stats = []
134+
for filepath, line_nums in sorted(changed_lines.items()):
135+
cov_map = file_coverage.get(filepath, {})
136+
executable = {ln for ln in line_nums if ln in cov_map}
137+
covered = {ln for ln in executable if cov_map[ln]}
138+
missing = sorted(executable - covered)
139+
140+
n_exec = len(executable)
141+
n_cov = len(covered)
142+
total_covered += n_cov
143+
total_changed += n_exec
144+
pct = (n_cov / n_exec * 100) if n_exec else -1
145+
file_stats.append((filepath, pct, n_cov, n_exec, missing))
146+
147+
file_stats.sort(key=lambda x: x[1])
148+
total_pct = (total_covered / total_changed * 100) if total_changed else 100
149+
150+
lines = []
151+
lines.append(f"## {status_icon(total_pct)} Diff Coverage — {total_pct:.0f}%")
152+
lines.append("")
153+
lines.append(
154+
f"`{make_bar(total_pct, 30)}` **{total_pct:.1f}%** ({total_covered} / {total_changed} changed lines covered)"
155+
)
156+
lines.append("")
157+
lines.append("| File | Coverage | Lines | Missing |")
158+
lines.append("|:-----|:--------:|:-----:|:--------|")
159+
160+
for filepath, pct, n_cov, n_exec, missing in file_stats:
161+
icon = status_icon(pct) if pct >= 0 else "\u26aa"
162+
pct_str = f"{pct:.0f}%" if pct >= 0 else "N/A"
163+
missing_str = ", ".join(f"L{ln}" for ln in missing[:20])
164+
if len(missing) > 20:
165+
missing_str += f" \u2026 +{len(missing) - 20} more"
166+
lines.append(f"| `{filepath}` | {icon} {pct_str} | {n_cov} / {n_exec} | {missing_str} |")
167+
168+
lines.append(f"| **Total** | **{total_pct:.1f}%** | **{total_covered} / {total_changed}** | |")
169+
lines.append("")
170+
return "\n".join(lines)
171+
172+
173+
def build_full_coverage_md(groups):
174+
"""Build full coverage markdown section (wrapped in <details>, collapsed by default)."""
175+
total_lines = sum(g["lines"] for g in groups.values())
176+
total_hits = sum(g["hits"] for g in groups.values())
177+
total_pct = (total_hits / total_lines * 100) if total_lines else 0
178+
179+
sorted_groups = sorted(
180+
groups.items(),
181+
key=lambda x: (x[1]["hits"] / x[1]["lines"] * 100) if x[1]["lines"] else 0,
182+
)
183+
184+
lines = []
185+
lines.append("<details>")
186+
lines.append(
187+
f"<summary><h3>{status_icon(total_pct)} Full Coverage Report — {total_pct:.0f}% ({total_hits} / {total_lines} lines)</h3></summary>"
188+
)
189+
lines.append("")
190+
lines.append(
191+
f"`{make_bar(total_pct, 30)}` **{total_pct:.1f}%** ({total_hits} / {total_lines} lines)"
192+
)
193+
lines.append("")
194+
lines.append("| Package | Coverage | Progress | Lines |")
195+
lines.append("|:--------|:--------:|:---------|------:|")
196+
197+
for key, g in sorted_groups:
198+
pct = (g["hits"] / g["lines"] * 100) if g["lines"] else 0
199+
icon = status_icon(pct)
200+
lines.append(
201+
f"| `{key}` | {icon} {pct:.1f}% | `{make_bar(pct)}` | {g['hits']} / {g['lines']} |"
202+
)
203+
204+
lines.append(f"| **Total** | **{total_pct:.1f}%** | | **{total_hits} / {total_lines}** |")
205+
lines.append("")
206+
lines.append("</details>")
207+
lines.append("")
208+
return "\n".join(lines)
209+
210+
211+
def main():
212+
xml_path = sys.argv[1] if len(sys.argv) > 1 else "coverage.xml"
213+
output_path = sys.argv[2] if len(sys.argv) > 2 else "coverage-summary.md"
214+
depth = int(sys.argv[3]) if len(sys.argv) > 3 else 2
215+
diff_branch = sys.argv[4] if len(sys.argv) > 4 else None
216+
217+
groups, file_coverage = parse_coverage_xml(xml_path, depth)
218+
219+
parts = []
220+
221+
if diff_branch:
222+
changed_lines = get_changed_lines(diff_branch)
223+
parts.append(build_diff_coverage_md(changed_lines, file_coverage))
224+
225+
parts.append(build_full_coverage_md(groups))
226+
227+
with open(output_path, "w") as f:
228+
f.write("\n".join(parts))
229+
230+
print(f"Coverage summary written to {output_path}")
231+
232+
233+
if __name__ == "__main__":
234+
main()

.github/workflows/codestyle.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ name: Codestyle checking
22

33
on:
44
workflow_call:
5+
secrets:
6+
AWS_CODEARTIFACT_READ_ACCESS_KEY:
7+
required: true
8+
AWS_CODEARTIFACT_READ_ACCESS_SECRET:
9+
required: true
510
workflow_dispatch:
611

712
jobs:
@@ -15,6 +20,11 @@ jobs:
1520
with:
1621
python-version: '3.10'
1722
cache: 'poetry'
23+
- name: Setup CodeArtifact auth for Poetry
24+
uses: ./.github/actions/setup-codeartifact-poetry-auth
25+
with:
26+
aws-access-key-id: ${{ secrets.AWS_CODEARTIFACT_READ_ACCESS_KEY }}
27+
aws-secret-access-key: ${{ secrets.AWS_CODEARTIFACT_READ_ACCESS_SECRET }}
1828
- name: Install black
1929
run: poetry install
2030
- name: Run black
@@ -30,6 +40,11 @@ jobs:
3040
with:
3141
python-version: '3.10'
3242
cache: 'poetry'
43+
- name: Setup CodeArtifact auth for Poetry
44+
uses: ./.github/actions/setup-codeartifact-poetry-auth
45+
with:
46+
aws-access-key-id: ${{ secrets.AWS_CODEARTIFACT_READ_ACCESS_KEY }}
47+
aws-secret-access-key: ${{ secrets.AWS_CODEARTIFACT_READ_ACCESS_SECRET }}
3348
- name: Install isort
3449
run: poetry install
3550
- name: Check isort version
@@ -57,6 +72,11 @@ jobs:
5772
with:
5873
python-version: '3.10'
5974
cache: 'poetry'
75+
- name: Setup CodeArtifact auth for Poetry
76+
uses: ./.github/actions/setup-codeartifact-poetry-auth
77+
with:
78+
aws-access-key-id: ${{ secrets.AWS_CODEARTIFACT_READ_ACCESS_KEY }}
79+
aws-secret-access-key: ${{ secrets.AWS_CODEARTIFACT_READ_ACCESS_SECRET }}
6080
- name: Install dependencies
6181
run: poetry install
6282
- name: Run pylint

.github/workflows/pypi-publish.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,14 @@ jobs:
328328
with:
329329
python-version: '3.10'
330330
cache: 'poetry'
331+
- name: Setup CodeArtifact auth for Poetry
332+
uses: ./.github/actions/setup-codeartifact-poetry-auth
333+
with:
334+
aws-access-key-id: ${{ secrets.AWS_CODEARTIFACT_READ_ACCESS_KEY }}
335+
aws-secret-access-key: ${{ secrets.AWS_CODEARTIFACT_READ_ACCESS_SECRET }}
336+
aws-region: us-east-1
337+
domain: flexcompute
338+
domain-owner: "625554095313"
331339
- name: Install dependencies
332340
run: poetry install
333341
- name: Pump version number

0 commit comments

Comments
 (0)