From 80dd46fe0c29fec7acc4e9c3b63dadcc3687d7bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:21:21 +0000 Subject: [PATCH 1/2] Initial plan From 811b1e67f16f3bd67897374852d7bae4b1a71273 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:45:30 +0000 Subject: [PATCH 2/2] Changes before error encountered Co-authored-by: lsmarsden <44576399+lsmarsden@users.noreply.github.com> --- modules/helpers/__tests__/hass.test.js | 41 ++++ modules/helpers/hass.js | 34 +-- .../__tests__/integration.test.js | 199 ++++++++++++++++++ modules/icon_border_progress/code.js | 17 +- modules/icon_border_progress/editor.yaml | 8 + modules/icon_border_progress/schema.yaml | 3 + .../CONFIG_OPTIONS.md | 8 - .../__tests__/integration.test.js | 146 +++++++++++++ modules/separator_as_progress_bar/code.js | 9 +- modules/separator_as_progress_bar/editor.yaml | 4 + modules/separator_as_progress_bar/schema.yaml | 3 + .../__tests__/code.test.js | 32 ++- modules/separator_as_timeline/code.js | 16 +- .../__pycache__/md_template.cpython-311.pyc | Bin 0 -> 6334 bytes .../template_renderer.cpython-311.pyc | Bin 0 -> 5444 bytes 15 files changed, 492 insertions(+), 28 deletions(-) create mode 100644 scripts/py/__pycache__/md_template.cpython-311.pyc create mode 100644 scripts/py/__pycache__/template_renderer.cpython-311.pyc diff --git a/modules/helpers/__tests__/hass.test.js b/modules/helpers/__tests__/hass.test.js index 7439b8b..605c981 100644 --- a/modules/helpers/__tests__/hass.test.js +++ b/modules/helpers/__tests__/hass.test.js @@ -109,3 +109,44 @@ describe("getState() with object input", () => { expect(getState({ foo: "bar" }, false)).toBeUndefined(); }); }); + +describe("getState() with DER precedence logic", () => { + it("should use DER syntax when present in object entity field, overriding separate attribute", () => { + // DER syntax in entity field should take precedence over separate attribute field + const input = { entity: "sensor.temperature[unit_of_measurement]", attribute: "calibrated" }; + expect(getState(input)).toBe("°C"); // Should use unit_of_measurement, not calibrated + }); + + it("should use separate attribute when no DER syntax in entity field", () => { + // When no DER syntax, should use separate attribute field + const input = { entity: "sensor.temperature", attribute: "calibrated" }; + expect(getState(input)).toBe(true); // Should use calibrated attribute + }); + + it("should handle DER syntax in entity_id field", () => { + // Test with entity_id instead of entity + const input = { entity_id: "light.living_room[brightness]", attribute: "some_other_attr" }; + expect(getState(input)).toBe(200); // Should use brightness from DER, not some_other_attr + }); + + it("should handle DER syntax with non-existent attribute gracefully", () => { + const input = { entity: "sensor.temperature[missing_attr]", attribute: "calibrated" }; + expect(getState(input, false)).toBeUndefined(); + }); + + it("should handle DER syntax with non-existent entity gracefully", () => { + const input = { entity: "sensor.unknown[some_attr]", attribute: "fallback_attr" }; + expect(getState(input)).toEqual(input); // Should return raw input when fallbackToRaw is true + expect(getState(input, false)).toBeUndefined(); + }); + + it("should still work with entity only (no attribute fields)", () => { + const input = { entity: "sensor.temperature" }; + expect(getState(input)).toBe("22.5"); // Should return entity state + }); + + it("should handle edge case where entity field contains DER but no separate attribute", () => { + const input = { entity: "sensor.temperature[unit_of_measurement]" }; + expect(getState(input)).toBe("°C"); // Should extract attribute from DER + }); +}); diff --git a/modules/helpers/hass.js b/modules/helpers/hass.js index eab6aa3..92f412f 100644 --- a/modules/helpers/hass.js +++ b/modules/helpers/hass.js @@ -9,10 +9,16 @@ * If the input is an object, the entityId is extracted from either the `entity_id` or `entity` field, * and the attribute is extracted from either the `attribute` or the `att` field. * + * **DER Precedence**: If the entity field contains DER syntax (`entity[attribute]`), this ALWAYS + * takes precedence over any separate attribute field. This ensures consistent behavior regardless + * of input format and allows UI editors to provide separate entity/attribute fields while maintaining + * full backward compatibility with DER syntax. + * * For any other inputs, the function directly returns the input value or undefined, depending on the value of `fallbackToRaw`. * - * @param {string} input - The input to retrieve the state or attribute for. Can be an entity ID, - * an entity ID with an attribute (e.g., `entity_id[attribute]`), or a raw value. + * @param {string|object} input - The input to retrieve the state or attribute for. Can be an entity ID, + * an entity ID with an attribute (e.g., `entity_id[attribute]`), an object with + * entity and optional attribute fields, or a raw value. * @param {boolean} [fallbackToRaw=true] - Determines whether to return the raw input value if the * state or entity is not found. Defaults to true. * @returns {string|undefined} - The state of the entity, the specified attribute value, the raw input, @@ -24,23 +30,27 @@ export const getState = (input, fallbackToRaw = true) => { let entityId, attribute; + // Step 1: Extract initial values based on input type if (typeof input === "object") { entityId = input.entity_id || input.entity; attribute = input.attribute || input.att; - } else { - // Pattern: entity_id[attribute] - const match = input.match(/^([A-z0-9_.]+)\[([A-z0-9_]+)]$/); + } else if (typeof input === "string") { + entityId = input; + } + + // Step 2: ALWAYS check for DER pattern and override if found + if (typeof entityId === "string") { + const match = entityId.match(/^([A-z0-9_.]+)\[([A-z0-9_]+)]$/); if (match) { - [, entityId, attribute] = match; - } else if (hass.states[input]) { - entityId = input; - } else { - return fallbackToRaw ? input : undefined; + [, entityId, attribute] = match; // DER takes precedence } } - const stateObj = hass.states[entityId]; - if (!stateObj) return fallbackToRaw ? input : undefined; + // Step 3: Validate and return + if (!entityId || !hass.states[entityId]) { + return fallbackToRaw ? input : undefined; + } + const stateObj = hass.states[entityId]; return attribute ? stateObj.attributes[attribute] : stateObj.state; }; diff --git a/modules/icon_border_progress/__tests__/integration.test.js b/modules/icon_border_progress/__tests__/integration.test.js index 313b39f..651d649 100644 --- a/modules/icon_border_progress/__tests__/integration.test.js +++ b/modules/icon_border_progress/__tests__/integration.test.js @@ -56,6 +56,26 @@ describe("icon_border_progress - Integration Tests", () => { "sensor.filament_abs_level": { state: "65" }, "sensor.filament_petg_level": { state: "35" }, "sensor.filament_cf_level": { state: "90" }, + // New entities for UI attribute testing + "sensor.device_status": { + state: "online", + attributes: { + battery_level: 85, + signal_strength: 95, + temperature: 23.5, + }, + }, + "sensor.system_monitor": { + state: "running", + attributes: { + cpu_usage: 45, + memory_usage: 60, + disk_usage: 75, + }, + }, + // Default start/end values for progress calculation + "sensor.start_value": { state: "0" }, + "sensor.end_value": { state: "100" }, }, }; @@ -1844,4 +1864,183 @@ describe("icon_border_progress - Integration Tests", () => { expect(mainIcon.style.background).toBe("rgb(128, 128, 128)"); }); }); + + describe("UI Editor Attribute Support", () => { + it("should use separate entity and attribute fields from UI editor", () => { + const config = [ + { + button: "sub-button-1", + source: "sensor.device_status", + source_attribute: "battery_level", + start: "sensor.start_value", + end: "sensor.end_value", + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + remaining_color: "#333", + background_color: "#111", + }, + ]; + + mockThis.config = { icon_border_progress: config }; + icon_border_progress.call(mockThis, mockCard, mockHass); + + const subButton1 = mockCard.querySelector(".bubble-sub-button-1"); + expect(subButton1).toBeTruthy(); + + // Should create SVG progress border based on battery_level attribute (85) + const svg = subButton1.querySelector(".stroke-dash-aligned-svg"); + expect(svg).toBeTruthy(); + + const progressPath = svg.querySelector(".progress-path"); + expect(progressPath).toBeTruthy(); + }); + + it("should prioritize DER syntax over separate attribute field", () => { + const config = [ + { + button: "sub-button-2", + source: "sensor.device_status[signal_strength]", // DER syntax for signal_strength + source_attribute: "battery_level", // Separate attribute field + start: "sensor.start_value", + end: "sensor.end_value", + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + remaining_color: "#333", + background_color: "#111", + }, + ]; + + mockThis.config = { icon_border_progress: config }; + icon_border_progress.call(mockThis, mockCard, mockHass); + + const subButton2 = mockCard.querySelector(".bubble-sub-button-2"); + expect(subButton2).toBeTruthy(); + + // Should create SVG progress border based on signal_strength (95) from DER, not battery_level (85) + const svg = subButton2.querySelector(".stroke-dash-aligned-svg"); + expect(svg).toBeTruthy(); + + const progressPath = svg.querySelector(".progress-path"); + expect(progressPath).toBeTruthy(); + }); + + it("should handle missing attribute gracefully with UI fields", () => { + const config = [ + { + button: "main", + source: "sensor.device_status", + source_attribute: "missing_attribute", + start: "sensor.start_value", + end: "sensor.end_value", + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + remaining_color: "#333", + background_color: "#111", + }, + ]; + + mockThis.config = { icon_border_progress: config }; + icon_border_progress.call(mockThis, mockCard, mockHass); + + const mainIcon = mockCard.querySelector(".bubble-icon-container"); + // Should set background color due to missing attribute (NaN progress → 0% → background color treatment) + expect(mainIcon.style.background).toBe("rgb(17, 17, 17)"); + }); + + it("should work with just entity field (no attribute) from UI", () => { + const config = [ + { + button: "sub-button-3", + source: "sensor.saros_10_battery", // Just entity, no attribute + start: "sensor.start_value", + end: "sensor.end_value", + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + remaining_color: "#333", + background_color: "#111", + }, + ]; + + mockThis.config = { icon_border_progress: config }; + icon_border_progress.call(mockThis, mockCard, mockHass); + + const subButton3 = mockCard.querySelector(".bubble-sub-button-3"); + expect(subButton3).toBeTruthy(); + + // Should create SVG progress border based on entity state value (75) + const svg = subButton3.querySelector(".stroke-dash-aligned-svg"); + expect(svg).toBeTruthy(); + + const progressPath = svg.querySelector(".progress-path"); + expect(progressPath).toBeTruthy(); + }); + + it("should maintain backward compatibility with YAML DER syntax", () => { + const config = [ + { + button: "sub-button-4", + source: "sensor.system_monitor[cpu_usage]", // YAML DER syntax + start: "sensor.start_value", + end: "sensor.end_value", + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + remaining_color: "#333", + background_color: "#111", + }, + ]; + + mockThis.config = { icon_border_progress: config }; + icon_border_progress.call(mockThis, mockCard, mockHass); + + const subButton4 = mockCard.querySelector(".bubble-sub-button-4"); + expect(subButton4).toBeTruthy(); + + // Should create SVG progress border based on cpu_usage attribute value (45) + const svg = subButton4.querySelector(".stroke-dash-aligned-svg"); + expect(svg).toBeTruthy(); + + const progressPath = svg.querySelector(".progress-path"); + expect(progressPath).toBeTruthy(); + }); + }); }); diff --git a/modules/icon_border_progress/code.js b/modules/icon_border_progress/code.js index 5909bc3..12172ed 100644 --- a/modules/icon_border_progress/code.js +++ b/modules/icon_border_progress/code.js @@ -31,7 +31,22 @@ export function icon_border_progress(card, hass) { } function calculateProgressValue(progressSource, buttonConfig) { - let progressValue = parseFloat(getState(progressSource)); + let progressValue; + + // Check if we have a separate source_attribute field + if (buttonConfig && buttonConfig.source_attribute) { + // Use object format for new UI attribute support + progressValue = parseFloat( + getState({ + entity: progressSource, + attribute: buttonConfig.source_attribute, + }), + ); + } else { + // Fall back to original behavior for backward compatibility + progressValue = parseFloat(getState(progressSource)); + } + let startValue = parseInt(getState(buttonConfig.start)); let endValue = parseInt(getState(buttonConfig.end)); diff --git a/modules/icon_border_progress/editor.yaml b/modules/icon_border_progress/editor.yaml index 6b3f38e..bab7c64 100644 --- a/modules/icon_border_progress/editor.yaml +++ b/modules/icon_border_progress/editor.yaml @@ -20,6 +20,10 @@ editor: label: "✨Source entity" selector: entity: {} + - name: source_attribute + label: "Source entity attribute (optional)" + selector: + attribute: {} - name: condition label: "Condition to show progress (see docs for additional condition configuration)" selector: @@ -106,6 +110,10 @@ editor: label: "✨Source entity" selector: entity: {} + - name: source_attribute + label: "Source entity attribute (optional)" + selector: + attribute: {} - name: condition label: "Condition to show progress (see docs for additional condition configuration)" selector: diff --git a/modules/icon_border_progress/schema.yaml b/modules/icon_border_progress/schema.yaml index 407b659..569acdd 100644 --- a/modules/icon_border_progress/schema.yaml +++ b/modules/icon_border_progress/schema.yaml @@ -17,6 +17,9 @@ definitions: type: string description: "Source entity for progress value" format: "entity[attribute]" + source_attribute: + type: string + description: "Attribute name to use from the source entity (optional - can also use entity[attribute] syntax)" entity: type: string description: "[DEPRECATED] Use 'source' instead. Source entity for progress value" diff --git a/modules/separator_as_progress_bar/CONFIG_OPTIONS.md b/modules/separator_as_progress_bar/CONFIG_OPTIONS.md index 3cc7ee3..1fa8734 100644 --- a/modules/separator_as_progress_bar/CONFIG_OPTIONS.md +++ b/modules/separator_as_progress_bar/CONFIG_OPTIONS.md @@ -1406,21 +1406,13 @@ Specific value: `"not"` Must be one of: - "none" - - "dotted" - - "dashed" - - "solid" - - "double" - - "groove" - - "ridge" - - "inset" - - "outset" ### 5.5. Property `color_stops` diff --git a/modules/separator_as_progress_bar/__tests__/integration.test.js b/modules/separator_as_progress_bar/__tests__/integration.test.js index 6f2ca90..87c7c72 100644 --- a/modules/separator_as_progress_bar/__tests__/integration.test.js +++ b/modules/separator_as_progress_bar/__tests__/integration.test.js @@ -28,6 +28,22 @@ describe("separator_as_progress_bar - Integration Tests", () => { "sensor.saros_10_cleaning_progress": { state: "80" }, "sensor.saros_10_current_room": { state: "Living Room" }, "sensor.saros_10_status": { state: "cleaning" }, + // New entities for UI attribute testing + "sensor.battery_device": { + state: "charging", + attributes: { + battery_level: 45, + temperature: 25.5, + voltage: 3.7, + }, + }, + "sensor.solar_system": { + state: "generating", + attributes: { + current_power: 85, + daily_energy: 12.5, + }, + }, // Edge case entities "sensor.invalid_progress": { state: "invalid" }, "sensor.negative_progress": { state: "-10" }, @@ -1865,4 +1881,134 @@ describe("separator_as_progress_bar - Integration Tests", () => { expect(wrapper.querySelector(".bubble-line-text.below-text")).toBeTruthy(); // Should exist again }); }); + + describe("UI Editor Attribute Support", () => { + it("should use separate entity and attribute fields from UI editor", () => { + const config = { + source: "sensor.battery_device", + source_attribute: "battery_level", + progress_style: { + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + }, + }; + + mockThis.config = { separator_as_progress_bar: config }; + separator_as_progress_bar.call(mockThis, mockCard, mockHass); + + const element = mockCard.querySelector(".bubble-line"); + expect(element.classList.contains("bubble-line-progress")).toBe(true); + expect(element.style.getPropertyValue("--progress-width")).toBe("45cqw"); // battery_level attribute value + }); + + it("should prioritize DER syntax over separate attribute field", () => { + const config = { + source: "sensor.battery_device[voltage]", // DER syntax for voltage + source_attribute: "battery_level", // Separate attribute field + progress_style: { + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + }, + }; + + mockThis.config = { separator_as_progress_bar: config }; + separator_as_progress_bar.call(mockThis, mockCard, mockHass); + + const element = mockCard.querySelector(".bubble-line"); + expect(element.classList.contains("bubble-line-progress")).toBe(true); + expect(element.style.getPropertyValue("--progress-width")).toBe("3.7cqw"); // voltage from DER, not battery_level + }); + + it("should handle missing attribute gracefully with UI fields", () => { + const config = { + source: "sensor.battery_device", + source_attribute: "missing_attribute", + progress_style: { + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + }, + }; + + mockThis.config = { separator_as_progress_bar: config }; + separator_as_progress_bar.call(mockThis, mockCard, mockHass); + + const element = mockCard.querySelector(".bubble-line"); + expect(element.classList.contains("bubble-line-progress")).toBe(true); + expect(element.style.getPropertyValue("--progress-width")).toBe("0cqw"); // Should default to 0 for NaN + }); + + it("should work with just entity field (no attribute) from UI", () => { + const config = { + source: "sensor.task_progress", // Just entity, no attribute + progress_style: { + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + }, + }; + + mockThis.config = { separator_as_progress_bar: config }; + separator_as_progress_bar.call(mockThis, mockCard, mockHass); + + const element = mockCard.querySelector(".bubble-line"); + expect(element.classList.contains("bubble-line-progress")).toBe(true); + expect(element.style.getPropertyValue("--progress-width")).toBe("75cqw"); // entity state value + }); + + it("should maintain backward compatibility with YAML DER syntax", () => { + const config = { + source: "sensor.solar_system[current_power]", // YAML DER syntax + progress_style: { + color_stops: [ + { + color: "red", + percent: 0, + }, + { + color: "green", + percent: 100, + }, + ], + }, + }; + + mockThis.config = { separator_as_progress_bar: config }; + separator_as_progress_bar.call(mockThis, mockCard, mockHass); + + const element = mockCard.querySelector(".bubble-line"); + expect(element.classList.contains("bubble-line-progress")).toBe(true); + expect(element.style.getPropertyValue("--progress-width")).toBe("85cqw"); // current_power attribute value + }); + }); }); diff --git a/modules/separator_as_progress_bar/code.js b/modules/separator_as_progress_bar/code.js index 1f9e41f..926404b 100644 --- a/modules/separator_as_progress_bar/code.js +++ b/modules/separator_as_progress_bar/code.js @@ -34,7 +34,14 @@ export function separator_as_progress_bar(card, hass) { } } - let progressValue = config.override ? config.override : parseFloat(getState(config.source)); + let progressValue = config.override + ? config.override + : parseFloat( + getState({ + entity: config.source, + attribute: config.source_attribute, + }), + ); if (isNaN(progressValue) || progressValue < 0 || progressValue > 100) { progressValue = 0; diff --git a/modules/separator_as_progress_bar/editor.yaml b/modules/separator_as_progress_bar/editor.yaml index 1387bc8..fdb7a2f 100644 --- a/modules/separator_as_progress_bar/editor.yaml +++ b/modules/separator_as_progress_bar/editor.yaml @@ -12,6 +12,10 @@ editor: label: "✨Source entity" selector: entity: {} + - name: source_attribute + label: "Source entity attribute (optional)" + selector: + attribute: {} - name: invert label: "Invert progress (bar decreases as progress completes)" default: false diff --git a/modules/separator_as_progress_bar/schema.yaml b/modules/separator_as_progress_bar/schema.yaml index e75e627..6c684ac 100644 --- a/modules/separator_as_progress_bar/schema.yaml +++ b/modules/separator_as_progress_bar/schema.yaml @@ -8,6 +8,9 @@ properties: type: string description: The entity or attribute source. format: entity[attribute] + source_attribute: + type: string + description: "Attribute name to use from the source entity (optional - can also use entity[attribute] syntax)" invert: type: boolean description: Invert the progress bar (bar decreases as progress completes) diff --git a/modules/separator_as_timeline/__tests__/code.test.js b/modules/separator_as_timeline/__tests__/code.test.js index 8e5ac5c..b3922bc 100644 --- a/modules/separator_as_timeline/__tests__/code.test.js +++ b/modules/separator_as_timeline/__tests__/code.test.js @@ -283,7 +283,13 @@ describe("separator_as_timeline - Unit Tests", () => { separator_as_timeline.call(mockThis, mockCard, mockHass); // Verify - expect(getState).toHaveBeenCalledWith("sensor.work_start", false); + expect(getState).toHaveBeenCalledWith( + { + entity: "sensor.work_start", + attribute: undefined, + }, + false, + ); expect(mockWrapper.appendChild).toHaveBeenCalled(); }); @@ -305,7 +311,13 @@ describe("separator_as_timeline - Unit Tests", () => { separator_as_timeline.call(mockThis, mockCard, mockHass); // Verify - Check that entity with attribute is queried correctly - expect(getState).toHaveBeenCalledWith("sensor.work_schedule[end_time]", false); + expect(getState).toHaveBeenCalledWith( + { + entity: "sensor.work_schedule", + attribute: "end_time", + }, + false, + ); expect(mockWrapper.appendChild).toHaveBeenCalled(); }); }); @@ -682,8 +694,20 @@ describe("separator_as_timeline - Unit Tests", () => { separator_as_timeline.call(mockThis, mockCard, mockHass); // Verify - Both entity queries should be made - expect(getState).toHaveBeenCalledWith("sensor.work_start", false); - expect(getState).toHaveBeenCalledWith("sensor.sleep_time[bedtime]", false); + expect(getState).toHaveBeenCalledWith( + { + entity: "sensor.work_start", + attribute: undefined, + }, + false, + ); + expect(getState).toHaveBeenCalledWith( + { + entity: "sensor.sleep_time", + attribute: "bedtime", + }, + false, + ); expect(mockWrapper.appendChild).toHaveBeenCalled(); }); diff --git a/modules/separator_as_timeline/code.js b/modules/separator_as_timeline/code.js index 9a4a613..3d6cf54 100644 --- a/modules/separator_as_timeline/code.js +++ b/modules/separator_as_timeline/code.js @@ -119,7 +119,13 @@ export function separator_as_timeline(card, hass) { //TODO - replace with a single start field. // this will be a breaking change, so we need to // implement auto-migration first - startTimeValue = getState(`${r.start_entity}${r.start_attribute ? `[${r.start_attribute}]` : ""}`, false); + startTimeValue = getState( + { + entity: r.start_entity, + attribute: r.start_attribute, + }, + false, + ); } else { startTimeValue = r.start; } @@ -127,7 +133,13 @@ export function separator_as_timeline(card, hass) { // Get end time (from entity if provided, otherwise from direct value) let endTimeValue; if (r.end_entity) { - endTimeValue = getState(`${r.end_entity}${r.end_attribute ? `[${r.end_attribute}]` : ""}`, false); + endTimeValue = getState( + { + entity: r.end_entity, + attribute: r.end_attribute, + }, + false, + ); } else { endTimeValue = r.end; } diff --git a/scripts/py/__pycache__/md_template.cpython-311.pyc b/scripts/py/__pycache__/md_template.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b684051897ba91278d45a67a801d61988f47163 GIT binary patch literal 6334 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 literal 0 HcmV?d00001 diff --git a/scripts/py/__pycache__/template_renderer.cpython-311.pyc b/scripts/py/__pycache__/template_renderer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..622fcc1164a2e206144bdc92e7eac8e6975ee259 GIT binary patch literal 5444 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 Ai~s-t literal 0 HcmV?d00001