Skip to content

Commit a591093

Browse files
📝 Add docstrings to ops/ci-gates-and-qa-docs (#6)
Docstrings generation was requested by @shayancoin. * #5 (comment) The following files were modified: * `backend/tests/test_error_envelopes.py` * `scripts/gen_glb_manifest.py` * `scripts/generate_reference_glbs.py` * `scripts/glb_validate.py` * `scripts/update_glb_metadata.py` * `tests/perf/k6-quote-cnc.js` Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent b0b5648 commit a591093

File tree

6 files changed

+248
-7
lines changed

6 files changed

+248
-7
lines changed

backend/tests/test_error_envelopes.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99

1010

1111
def _is_error_shape(payload: dict) -> bool:
12+
"""
13+
Check whether a payload matches the error envelope shape: 'ok' equals False and 'error' is a dict containing 'code' and 'message'.
14+
15+
Parameters:
16+
payload (dict): The response payload to validate.
17+
18+
Returns:
19+
bool: True if the payload has 'ok' set to False and an 'error' dict with both 'code' and 'message', False otherwise.
20+
"""
1221
if not isinstance(payload, dict):
1322
return False
1423
if payload.get('ok') is not False:
@@ -28,10 +37,15 @@ def test_quote_invalid_payload_envelope() -> None:
2837

2938

3039
def test_cnc_invalid_payload_envelope() -> None:
40+
"""
41+
Verifies the /api/cnc/export endpoint returns a standardized error envelope for an invalid JSON request body.
42+
43+
Sends a request with a non-JSON payload and asserts the HTTP status is 400 or 422 and the response body matches the expected error shape (an object with `ok: False` and an `error` containing `code` and `message`).
44+
"""
3145
response = client.post(
3246
'/api/cnc/export',
3347
data='not-json',
3448
headers={'Content-Type': 'application/json'},
3549
)
3650
assert response.status_code in (400, 422)
37-
assert _is_error_shape(response.json())
51+
assert _is_error_shape(response.json())

scripts/gen_glb_manifest.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@
1313

1414

1515
def manifest_entry(path: Path) -> dict:
16+
"""
17+
Builds a manifest entry for the GLB file at the given path.
18+
19+
Parameters:
20+
path (Path): Path to the GLB file.
21+
22+
Returns:
23+
dict: Dictionary with keys:
24+
- "file": asset path as "/models/<filename>".
25+
- "sha256": hexadecimal SHA-256 digest of the file contents.
26+
- "bytes": file size in bytes.
27+
"""
1628
data = path.read_bytes()
1729
return {
1830
"file": "/models/" + path.name,
@@ -22,6 +34,14 @@ def manifest_entry(path: Path) -> dict:
2234

2335

2436
def main() -> int:
37+
"""
38+
Generate and print a JSON manifest of GLB model assets found under MODELS_PATTERN.
39+
40+
Discovers matching .glb files, builds manifest entries for each using `manifest_entry`, writes the manifest as pretty-printed JSON to standard output, and returns an exit code.
41+
42+
Returns:
43+
int: Exit code `0` on success.
44+
"""
2545
paths = sorted(MODELS_PATTERN.parent.glob(MODELS_PATTERN.name))
2646
entries = [manifest_entry(path) for path in paths]
2747
manifest = {"models": entries}
@@ -30,4 +50,4 @@ def main() -> int:
3050

3151

3252
if __name__ == "__main__":
33-
raise SystemExit(main())
53+
raise SystemExit(main())

scripts/generate_reference_glbs.py

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,63 @@
3333

3434

3535
def pack_f32(values: Iterable[float]) -> bytes:
36+
"""
37+
Pack an iterable of numbers into a binary blob of little-endian 32-bit floats.
38+
39+
Parameters:
40+
values (Iterable[float]): Sequence of numeric values to encode; each value is converted to a 32-bit IEEE 754 float.
41+
42+
Returns:
43+
bytes: Concatenation of little-endian 32-bit float representations of the input values.
44+
"""
3645
return b"".join(struct.pack("<f", float(v)) for v in values)
3746

3847

3948
def pack_u16(values: Iterable[int]) -> bytes:
49+
"""
50+
Pack an iterable of integers into a concatenated little-endian 16-bit unsigned integer byte sequence.
51+
52+
Parameters:
53+
values (Iterable[int]): Sequence of values to encode; each value will be converted to an integer and encoded as a 16-bit unsigned little-endian value.
54+
55+
Returns:
56+
bytes: Concatenated binary representation of the encoded values.
57+
"""
4058
return b"".join(struct.pack("<H", int(v)) for v in values)
4159

4260

4361
def pad4(data: bytes, pad: bytes = b" ") -> bytes:
62+
"""
63+
Pad a byte string so its length is a multiple of 4 by appending a specified byte.
64+
65+
Parameters:
66+
data (bytes): The input byte string to be padded.
67+
pad (bytes): The byte sequence to append to reach a 4-byte boundary (defaults to a single space).
68+
69+
Returns:
70+
bytes: The input data padded on the right so its length is divisible by 4.
71+
"""
4472
remainder = (-len(data)) % 4
4573
if remainder:
4674
data += pad * remainder
4775
return data
4876

4977

5078
def build_box(width: float, height: float, depth: float, subdivisions: int = 2) -> Tuple[List[float], List[float], List[int]]:
51-
"""Create a box mesh with per-face subdivisions so LOD simplification has room to operate."""
79+
"""
80+
Generate a subdivided box mesh aligned with the model origin.
81+
82+
Parameters:
83+
width (float): Box extent along the X axis.
84+
height (float): Box extent along the Y axis.
85+
depth (float): Box extent along the Z axis.
86+
subdivisions (int): Number of subdivisions per face edge; 0 yields one quad per face.
87+
88+
Returns:
89+
positions (List[float]): Flat list of vertex positions [x, y, z, ...].
90+
texcoords (List[float]): Flat list of per-vertex UV coordinates [u, v, ...].
91+
indices (List[int]): Triangle index list referencing vertices (triplets per triangle).
92+
"""
5293

5394
w, h, d = float(width), float(height), float(depth)
5495
positions: List[float] = []
@@ -98,12 +139,34 @@ def build_box(width: float, height: float, depth: float, subdivisions: int = 2)
98139
def create_glb_document(
99140
code: str, width: int, height: int, depth: int
100141
) -> Tuple[Dict, bytes]:
142+
"""
143+
Builds a glTF 2.0 document dictionary and a single concatenated binary buffer for a minimal box mesh with embedded 4x4 PNG textures and module metadata.
144+
145+
Returns:
146+
tuple: A pair (document, binary_blob) where `document` is the glTF JSON-compatible dictionary describing asset, scenes, nodes, meshes, materials, textures, images, bufferViews, accessors, and a buffer entry; and `binary_blob` is the bytes object containing the concatenated, 4-byte-aligned binary data for vertex attributes, indices, and embedded image files.
147+
"""
101148
positions, texcoords, indices = build_box(width, height, depth)
102149
position_blob = pack_f32(positions)
103150
texcoord_blob = pack_f32(texcoords)
104151
index_blob = pack_u16(indices)
105152

106153
def add_view(blob: bytes, *, target: int | None = None) -> Tuple[int, bytes]:
154+
"""
155+
Add a binary buffer view to the accumulating GLB buffer and return its index and padded bytes.
156+
157+
Parameters:
158+
blob (bytes): Raw binary payload to append to the shared buffer.
159+
target (int | None): Optional GLTF bufferView `target` (e.g. 34962 for ARRAY_BUFFER); if None, no target is set.
160+
161+
Returns:
162+
tuple:
163+
index (int): Index of the newly created bufferView in `buffer_views`.
164+
padded (bytes): The input `blob` padded to a 4-byte boundary that was appended to the shared buffer.
165+
166+
Side effects:
167+
Appends the padded bytes to the nonlocal `buffer_bytes`, appends a bufferView entry to `buffer_views`,
168+
and advances the nonlocal `byte_offset`.
169+
"""
107170
nonlocal buffer_bytes, byte_offset
108171
view = {"buffer": 0, "byteOffset": byte_offset, "byteLength": len(blob)}
109172
if target is not None:
@@ -233,6 +296,14 @@ def add_view(blob: bytes, *, target: int | None = None) -> Tuple[int, bytes]:
233296

234297

235298
def write_glb(path: Path, document: Dict, binary_blob: bytes) -> None:
299+
"""
300+
Write a glTF 2.0 binary (GLB) file to the given path using the provided glTF JSON document and binary blob.
301+
302+
Parameters:
303+
path (Path): Filesystem path where the GLB file will be written; parent directories will be created if missing.
304+
document (Dict): glTF JSON document structure to be serialized into the GLB JSON chunk.
305+
binary_blob (bytes): Binary buffer data to be written into the GLB BIN chunk.
306+
"""
236307
json_bytes = pad4(json.dumps(document, separators=(",", ":")).encode("utf-8"))
237308
bin_bytes = pad4(binary_blob, b"\x00")
238309

@@ -251,6 +322,11 @@ def write_glb(path: Path, document: Dict, binary_blob: bytes) -> None:
251322

252323

253324
def main() -> None:
325+
"""
326+
Generate three deterministic reference GLB files and write them to the frontend public models directory.
327+
328+
Creates the target models directory if missing, removes a legacy BaseCabinet600.glb if present, and produces three GLB assets with fixed codes and dimensions — "base_600" (600×720×580), "wall_900" (900×720×360), and "tall_600" (600×2100×580) — by calling create_glb_document for each and writing the resulting files with write_glb. Prints the relative output path for each written file.
329+
"""
254330
MODELS_DIR.mkdir(parents=True, exist_ok=True)
255331
legacy = MODELS_DIR / "BaseCabinet600.glb"
256332
if legacy.exists():
@@ -269,4 +345,4 @@ def main() -> None:
269345

270346

271347
if __name__ == "__main__":
272-
main()
348+
main()

scripts/glb_validate.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,29 @@ class Issue:
2626
message: str
2727

2828
def __str__(self) -> str:
29+
"""
30+
Return a compact, human-readable representation of the issue including a severity icon, code, and message.
31+
32+
Returns:
33+
str: Formatted string "<icon> [<code>] <message>" where the icon is "❌" for severity "ERROR" and "⚠️" for any other severity.
34+
"""
2935
icon = "❌" if self.severity == "ERROR" else "⚠️"
3036
return f"{icon} [{self.code}] {self.message}"
3137

3238

3339
def load_glb(path: Path) -> Dict:
40+
"""
41+
Load a GLB file and return its parsed JSON content.
42+
43+
Parameters:
44+
path (Path): Filesystem path to a .glb file.
45+
46+
Returns:
47+
dict: The parsed JSON chunk contained in the GLB file.
48+
49+
Raises:
50+
ValueError: If the file is not a valid GLB (missing GLB header) or if the JSON chunk is absent.
51+
"""
3452
data = path.read_bytes()
3553
if data[:4] != b"glTF":
3654
raise ValueError("not a GLB file")
@@ -50,6 +68,15 @@ def load_glb(path: Path) -> Dict:
5068

5169

5270
def gather_root_nodes(doc: Dict) -> List[int]:
71+
"""
72+
Retrieve the node indices for the active scene's root nodes.
73+
74+
Parameters:
75+
doc (Dict): Parsed glTF JSON document.
76+
77+
Returns:
78+
root_nodes (List[int]): List of node indices for the active scene. Returns an empty list if the document has no scenes.
79+
"""
5380
scenes = doc.get("scenes") or []
5481
scene_idx = doc.get("scene", 0)
5582
if not scenes:
@@ -58,6 +85,12 @@ def gather_root_nodes(doc: Dict) -> List[int]:
5885

5986

6087
def compute_bounds(doc: Dict) -> Optional[Tuple[List[float], List[float]]]:
88+
"""
89+
Compute the axis-aligned bounding box across all mesh primitives that provide POSITION accessor min/max.
90+
91+
Returns:
92+
A tuple `(min_vals, max_vals)` where each is a 3-element list `[x, y, z]` representing the aggregated minimum and maximum coordinates, or `None` if no POSITION accessors with valid `min`/`max` were found.
93+
"""
6194
accessors = doc.get("accessors") or []
6295
meshes = doc.get("meshes") or []
6396
min_vals = [math.inf, math.inf, math.inf]
@@ -81,6 +114,14 @@ def compute_bounds(doc: Dict) -> Optional[Tuple[List[float], List[float]]]:
81114

82115

83116
def triangle_count(doc: Dict) -> int:
117+
"""
118+
Compute the total number of triangles across all meshes in a glTF JSON document.
119+
120+
For each mesh primitive, uses the indices accessor count divided by three when present; otherwise uses the POSITION accessor count divided by three. Missing accessors or missing counts are treated as zero contribution.
121+
122+
Returns:
123+
int: Total triangle count summed across all mesh primitives.
124+
"""
84125
total = 0
85126
accessors = doc.get("accessors") or []
86127
for mesh in doc.get("meshes") or []:
@@ -98,6 +139,22 @@ def triangle_count(doc: Dict) -> int:
98139

99140

100141
def validate(path: Path, fail_on_warning: bool, warn_threshold: int) -> Tuple[int, int, List[Issue]]:
142+
"""
143+
Validate a Paform GLB asset and collect detected issues.
144+
145+
Performs schema, metadata, material, mesh, bounds, panel type, and LOD validations and aggregates detected issues.
146+
147+
Parameters:
148+
path (Path): Filesystem path to the GLB file to validate.
149+
fail_on_warning (bool): If True and there are warnings but no errors, treat the run as an error (affects returned counts and issue list).
150+
warn_threshold (int): If not None and the number of warnings exceeds this threshold, a WARN_THRESHOLD error will be added.
151+
152+
Returns:
153+
Tuple[int, int, List[Issue]]: A tuple of (num_errors, num_warnings, issues) where:
154+
- num_errors is the number of issues with severity "ERROR" after applying warn_threshold and fail_on_warning rules.
155+
- num_warnings is the number of issues with severity "WARN" originally detected.
156+
- issues is the list of Issue objects in the order they were detected, with any additional ERROR issues appended for warn-threshold or fail-on-warning promotion.
157+
"""
101158
issues: List[Issue] = []
102159
try:
103160
doc = load_glb(path)
@@ -234,6 +291,15 @@ def validate(path: Path, fail_on_warning: bool, warn_threshold: int) -> Tuple[in
234291

235292

236293
def main(argv: Sequence[str] | None = None) -> int:
294+
"""
295+
Run the command-line GLB validator for the given paths and return a process exit code.
296+
297+
Parameters:
298+
argv (Sequence[str] | None): Optional list of CLI arguments to parse; if None, reads from sys.argv.
299+
300+
Returns:
301+
int: Exit status code — 0 if all files passed, 1 if warnings were escalated to errors (no other errors), 2 if any validation errors were found.
302+
"""
237303
parser = argparse.ArgumentParser(description="Validate Paform GLB files")
238304
parser.add_argument("paths", nargs="+", help="paths or globs to GLB files")
239305
parser.add_argument("--warn-threshold", type=int, default=5, help="maximum allowed warnings before failure")
@@ -261,4 +327,4 @@ def main(argv: Sequence[str] | None = None) -> int:
261327

262328

263329
if __name__ == "__main__":
264-
sys.exit(main())
330+
sys.exit(main())

0 commit comments

Comments
 (0)