Good good progress
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
<!-- Name and Date Section -->
|
<!-- Name and Date Section -->
|
||||||
<div class="text-right mt-16">
|
<div class="text-right mt-16">
|
||||||
<h2 class="text-4xl font-bold tracking-wider mb-2">
|
<h2 class="text-4xl font-bold tracking-wider mb-2">
|
||||||
{{ first_name|upper }}
|
{{ name|upper }}
|
||||||
</h2>
|
</h2>
|
||||||
<h2 class="text-4xl font-bold tracking-wider mb-6">
|
<h2 class="text-4xl font-bold tracking-wider mb-6">
|
||||||
{{ surname|upper }}
|
{{ surname|upper }}
|
||||||
|
|||||||
@@ -29,10 +29,6 @@
|
|||||||
|
|
||||||
<!-- Personalized Heart Rate Zones Section -->
|
<!-- Personalized Heart Rate Zones Section -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h3 class="text-xl font-bold text-black mb-6 text-center">
|
|
||||||
Personalized Heart Rate Zones
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Heart Rate Zones Table -->
|
<!-- Heart Rate Zones Table -->
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<img
|
<img
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -804,8 +804,11 @@ class ContextGenerator:
|
|||||||
)
|
)
|
||||||
zone_df = self.pnoe_df[mask]
|
zone_df = self.pnoe_df[mask]
|
||||||
|
|
||||||
|
# HR BPM Range - match notebook exactly
|
||||||
|
hr_bpm_str = f"{int(start)}-{int(end)} bpm"
|
||||||
|
|
||||||
if not zone_df.empty:
|
if not zone_df.empty:
|
||||||
# Speed calculation
|
# Speed calculation - match notebook exactly
|
||||||
speed_series = zone_df[zone_df["Speed"] > 0.1]["Speed"]
|
speed_series = zone_df[zone_df["Speed"] > 0.1]["Speed"]
|
||||||
if not speed_series.empty:
|
if not speed_series.empty:
|
||||||
min_speed = speed_series.min()
|
min_speed = speed_series.min()
|
||||||
@@ -823,23 +826,20 @@ class ContextGenerator:
|
|||||||
if min_pace_m == max_pace_m and min_pace_s == max_pace_s:
|
if min_pace_m == max_pace_m and min_pace_s == max_pace_s:
|
||||||
pace_str = f"{min_pace_m}:{min_pace_s:02d} min/km Pace"
|
pace_str = f"{min_pace_m}:{min_pace_s:02d} min/km Pace"
|
||||||
else:
|
else:
|
||||||
pace_str = (
|
pace_str = f"{max_pace_m}:{max_pace_s:02d}-{min_pace_m}:{min_pace_s:02d}\nmin/km Pace"
|
||||||
f"{max_pace_m}:{max_pace_s:02d}-{min_pace_m}:{min_pace_s:02d}\n"
|
|
||||||
f"min/km Pace"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
speed_str = "-\n2% Incline"
|
speed_str = "-\n2% Incline"
|
||||||
pace_str = "-"
|
pace_str = "-"
|
||||||
|
|
||||||
# Calories (using raw EE)
|
# Calories (using raw EE) - match notebook exactly
|
||||||
avg_cals = zone_df["EE(kcal/min)"].mean()
|
avg_cals = zone_df["EE(kcal/min)"].mean()
|
||||||
calories_str = f"Avg:\n{avg_cals:.1f} kcals/minute"
|
calories_str = f"Avg:\n{avg_cals:.1f} kcals/minute"
|
||||||
|
|
||||||
# Carb utilization (g/min)
|
# Carb utilization (g/min) - match notebook exactly
|
||||||
avg_carbs_g = zone_df["CHO"].mean() / 4
|
avg_carbs_g = zone_df["CHO"].mean() / 4
|
||||||
carb_str = f"Avg: {avg_carbs_g:.1f}g/min\nCarb Utilization"
|
carb_str = f"Avg: {avg_carbs_g:.1f}g/min\nCarb Utilization"
|
||||||
|
|
||||||
# Breathing
|
# Breathing - match notebook exactly
|
||||||
avg_breaths = zone_df["BF(bpm)_smoothed"].mean()
|
avg_breaths = zone_df["BF(bpm)_smoothed"].mean()
|
||||||
breath_str = (
|
breath_str = (
|
||||||
f"Avg: {int(avg_breaths)} breaths\n{ideal_breath_ranges[i]}"
|
f"Avg: {int(avg_breaths)} breaths\n{ideal_breath_ranges[i]}"
|
||||||
@@ -854,7 +854,7 @@ class ContextGenerator:
|
|||||||
zone_data.append(
|
zone_data.append(
|
||||||
{
|
{
|
||||||
"zone_name": name,
|
"zone_name": name,
|
||||||
"hr_bpm": f"{int(start)}-{int(end)}bpm",
|
"hr_bpm": hr_bpm_str,
|
||||||
"speed": speed_str,
|
"speed": speed_str,
|
||||||
"pace": pace_str,
|
"pace": pace_str,
|
||||||
"calories": calories_str,
|
"calories": calories_str,
|
||||||
@@ -1246,18 +1246,18 @@ class ContextGenerator:
|
|||||||
hr_zones_columns = ["Zone 1", "Zone 2", "Zone 3", "Zone 4", "Zone 5"]
|
hr_zones_columns = ["Zone 1", "Zone 2", "Zone 3", "Zone 4", "Zone 5"]
|
||||||
hr_zones_data = [
|
hr_zones_data = [
|
||||||
[
|
[
|
||||||
"Improves health and recovery capacity",
|
"Improves health and\nrecovery capacity",
|
||||||
"Improves endurance and fat burning",
|
"Improves endurance\nand fat burning",
|
||||||
"Improves Aerobic fitness",
|
"Improves Aerobic\nfitness",
|
||||||
"Improves maximum performance capacity",
|
"Improves maximum\nperformance capacity",
|
||||||
"Develops maximum performance and speed",
|
"Develops maximum\nperformance and speed",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"55-65% of Max Heart Rate",
|
"55-65% of Max Heart Rate",
|
||||||
"65-75% of Max Heart Rate",
|
"65-75% of Max Heart Rate",
|
||||||
"80-85% of Max Heart Rate",
|
"80-85% of Max Heart Rate",
|
||||||
"85-88% of Max Heart Rate",
|
"85-88% of Max Heart Rate",
|
||||||
"90% of Max Heart Rate",
|
"90%+ of Max Heart Rate",
|
||||||
],
|
],
|
||||||
[zone_metrics["zones"][i]["hr_bpm"] for i in range(5)],
|
[zone_metrics["zones"][i]["hr_bpm"] for i in range(5)],
|
||||||
[zone_metrics["zones"][i]["speed"] for i in range(5)],
|
[zone_metrics["zones"][i]["speed"] for i in range(5)],
|
||||||
@@ -1266,22 +1266,12 @@ class ContextGenerator:
|
|||||||
[zone_metrics["zones"][i]["carb"] for i in range(5)],
|
[zone_metrics["zones"][i]["carb"] for i in range(5)],
|
||||||
[zone_metrics["zones"][i]["breathing"] for i in range(5)],
|
[zone_metrics["zones"][i]["breathing"] for i in range(5)],
|
||||||
]
|
]
|
||||||
hr_zones_colors = [
|
# Colors are now handled directly in the graph generator to match notebook
|
||||||
["#ffffff"] * 5,
|
# No need to pass cell_colors
|
||||||
["#ffffff"] * 5,
|
|
||||||
["#ffcdd2", "#ffcdd2", "#fff9c4", "#c8e6c9", "#c8e6c9"],
|
|
||||||
["#ffffff"] * 5,
|
|
||||||
["#ffffff"] * 5,
|
|
||||||
["#ffffff"] * 5,
|
|
||||||
["#ffffff"] * 5,
|
|
||||||
["#ffcdd2", "#ffcdd2", "#fff9c4", "#c8e6c9", "#c8e6c9"],
|
|
||||||
]
|
|
||||||
|
|
||||||
contexts["page_8"]["hr_zones_table"] = (
|
contexts["page_8"]["hr_zones_table"] = (
|
||||||
graph_generator.generate_heart_rate_zones_table(
|
graph_generator.generate_heart_rate_zones_table(
|
||||||
data=hr_zones_data,
|
data=hr_zones_data,
|
||||||
columns=hr_zones_columns,
|
columns=hr_zones_columns,
|
||||||
cell_colors=hr_zones_colors,
|
|
||||||
save_as_base64=True,
|
save_as_base64=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1465,67 +1465,60 @@ class GraphGenerator:
|
|||||||
Args:
|
Args:
|
||||||
data: List of rows (each row is a list of values)
|
data: List of rows (each row is a list of values)
|
||||||
columns: List of column headers (Zone 1-5)
|
columns: List of column headers (Zone 1-5)
|
||||||
cell_colors: Optional matrix of cell colors
|
cell_colors: Optional matrix of cell colors (IGNORED - using notebook colors)
|
||||||
save_as_base64: If True, return base64 string
|
save_as_base64: If True, return base64 string
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Base64 string or file path
|
Base64 string or file path
|
||||||
"""
|
"""
|
||||||
import io
|
import io
|
||||||
import textwrap
|
|
||||||
|
|
||||||
# Optimal sizing for HR Zones table (5 columns, 8 rows)
|
# Optimal sizing for HR Zones table (5 columns, 8 rows) - match notebook exactly
|
||||||
fig, ax = plt.subplots(figsize=(14, 8))
|
fig, ax = plt.subplots(figsize=(12, 8))
|
||||||
ax.axis("off")
|
ax.axis("off")
|
||||||
|
|
||||||
# Column widths - slightly wider for first column which has longer text
|
# Data comes pre-formatted with newlines from context_generator - use as-is
|
||||||
# col_widths = [0.24, 0.19, 0.19, 0.19, 0.19]
|
# No text wrapping needed
|
||||||
|
|
||||||
# Wrap text in cells for better readability
|
# Create table without rowLabels - match notebook exactly
|
||||||
wrapped_data = []
|
|
||||||
for row in data:
|
|
||||||
wrapped_row = []
|
|
||||||
for i, cell in enumerate(row):
|
|
||||||
cell_text = str(cell)
|
|
||||||
# First column needs more wrapping space
|
|
||||||
wrap_width = 35 if i == 0 else 25
|
|
||||||
wrapped_text = "\n".join(textwrap.wrap(cell_text, width=wrap_width))
|
|
||||||
wrapped_row.append(wrapped_text)
|
|
||||||
wrapped_data.append(wrapped_row)
|
|
||||||
|
|
||||||
# Create table
|
|
||||||
table = ax.table(
|
table = ax.table(
|
||||||
cellText=wrapped_data,
|
cellText=data,
|
||||||
colLabels=columns,
|
colLabels=columns,
|
||||||
cellLoc="center",
|
|
||||||
loc="center",
|
loc="center",
|
||||||
colColours=["#4dd0e1"] * len(columns),
|
cellLoc="center",
|
||||||
# colWidths=col_widths,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Style the table
|
# Style the table - match notebook exactly
|
||||||
table.auto_set_font_size(False)
|
table.auto_set_font_size(False)
|
||||||
table.set_fontsize(11)
|
table.set_fontsize(10)
|
||||||
table.scale(1, 2.0)
|
table.scale(1, 3.5) # Increased vertical scale for multi-line text
|
||||||
|
|
||||||
# Apply cell colors
|
# Header row styling
|
||||||
if cell_colors:
|
for j, label in enumerate(columns):
|
||||||
for i, row_colors in enumerate(cell_colors):
|
cell = table[(0, j)]
|
||||||
for j, color in enumerate(row_colors):
|
cell.set_facecolor("#7dd3fc") # cyan-300
|
||||||
if color and j < len(columns):
|
cell.set_text_props(weight="bold")
|
||||||
cell = table[(i + 1, j)]
|
|
||||||
cell.set_facecolor(color)
|
|
||||||
|
|
||||||
# Style all cells
|
# Row specific styling - match notebook colors exactly
|
||||||
for (row, col), cell in table.get_celld().items():
|
colors = ["#fecaca", "#fecaca", "#fef08a", "#bbf7d0", "#bbf7d0"]
|
||||||
if row == 0:
|
|
||||||
cell.set_text_props(weight="bold", fontsize=13)
|
# HR BPM row is at index 2 (0-based in data) -> row 3 in table (0 is header)
|
||||||
cell.set_edgecolor("#333333")
|
for j in range(len(columns)):
|
||||||
cell.set_linewidth(1.5)
|
cell = table[(3, j)]
|
||||||
else:
|
cell.set_facecolor(colors[j])
|
||||||
cell.set_edgecolor("#666666")
|
cell.set_text_props(weight="bold")
|
||||||
cell.set_linewidth(0.8)
|
|
||||||
cell.set_text_props(fontsize=10)
|
# Breathing row is at index 7 -> row 8 in table
|
||||||
|
for j in range(len(columns)):
|
||||||
|
cell = table[(8, j)]
|
||||||
|
cell.set_facecolor(colors[j])
|
||||||
|
cell.set_text_props(weight="bold")
|
||||||
|
|
||||||
|
# Add title matching notebook
|
||||||
|
plt.title(
|
||||||
|
"Personalized Heart Rate Zones", fontsize=16, fontweight="bold", pad=5
|
||||||
|
)
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
if save_as_base64:
|
if save_as_base64:
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
@@ -1535,7 +1528,6 @@ class GraphGenerator:
|
|||||||
bbox_inches="tight",
|
bbox_inches="tight",
|
||||||
dpi=300,
|
dpi=300,
|
||||||
facecolor="white",
|
facecolor="white",
|
||||||
pad_inches=0.1,
|
|
||||||
)
|
)
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
@@ -1549,7 +1541,6 @@ class GraphGenerator:
|
|||||||
bbox_inches="tight",
|
bbox_inches="tight",
|
||||||
dpi=300,
|
dpi=300,
|
||||||
facecolor="white",
|
facecolor="white",
|
||||||
pad_inches=0.1,
|
|
||||||
)
|
)
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
return str(output_path)
|
return str(output_path)
|
||||||
|
|||||||
+22
-22
@@ -2,7 +2,7 @@
|
|||||||
"cells": [
|
"cells": [
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 1,
|
"execution_count": 21,
|
||||||
"id": "63f43af5",
|
"id": "63f43af5",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 2,
|
"execution_count": 22,
|
||||||
"id": "97da3d1c",
|
"id": "97da3d1c",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 3,
|
"execution_count": 23,
|
||||||
"id": "b0ee2af1",
|
"id": "b0ee2af1",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"name": "stderr",
|
"name": "stderr",
|
||||||
"output_type": "stream",
|
"output_type": "stream",
|
||||||
"text": [
|
"text": [
|
||||||
"/tmp/ipykernel_103354/3076306744.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n",
|
"/tmp/ipykernel_150441/3076306744.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n",
|
||||||
" df = df.apply(pd.to_numeric, errors='ignore')\n"
|
" df = df.apply(pd.to_numeric, errors='ignore')\n"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 4,
|
"execution_count": 24,
|
||||||
"id": "99116a35",
|
"id": "99116a35",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 5,
|
"execution_count": 25,
|
||||||
"id": "fbd292c3",
|
"id": "fbd292c3",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 6,
|
"execution_count": 26,
|
||||||
"id": "4c439b2c",
|
"id": "4c439b2c",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 7,
|
"execution_count": 27,
|
||||||
"id": "a565f1b3",
|
"id": "a565f1b3",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 8,
|
"execution_count": 28,
|
||||||
"id": "470e871e",
|
"id": "470e871e",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -431,7 +431,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 9,
|
"execution_count": 29,
|
||||||
"id": "0ab6f0b0",
|
"id": "0ab6f0b0",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -577,7 +577,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 10,
|
"execution_count": 30,
|
||||||
"id": "ef8bc7ac",
|
"id": "ef8bc7ac",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -638,7 +638,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 11,
|
"execution_count": 31,
|
||||||
"id": "06244aa2",
|
"id": "06244aa2",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -753,7 +753,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 12,
|
"execution_count": 32,
|
||||||
"id": "8a1878a0",
|
"id": "8a1878a0",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -832,7 +832,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 13,
|
"execution_count": 33,
|
||||||
"id": "7361fb05",
|
"id": "7361fb05",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -893,7 +893,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 14,
|
"execution_count": 34,
|
||||||
"id": "c89478ff",
|
"id": "c89478ff",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -953,7 +953,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 15,
|
"execution_count": 35,
|
||||||
"id": "1db16040",
|
"id": "1db16040",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -1028,7 +1028,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 16,
|
"execution_count": 36,
|
||||||
"id": "c8ad6076",
|
"id": "c8ad6076",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -1109,7 +1109,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 17,
|
"execution_count": 37,
|
||||||
"id": "25327cc1",
|
"id": "25327cc1",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -1415,7 +1415,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 24,
|
"execution_count": 38,
|
||||||
"id": "c46b53f0",
|
"id": "c46b53f0",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -1731,7 +1731,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 19,
|
"execution_count": 39,
|
||||||
"id": "84addc63",
|
"id": "84addc63",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -1869,7 +1869,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 20,
|
"execution_count": 40,
|
||||||
"id": "f324fe94",
|
"id": "f324fe94",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -2066,7 +2066,7 @@
|
|||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"kernelspec": {
|
"kernelspec": {
|
||||||
"display_name": ".venv",
|
"display_name": "report-generation",
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"name": "python3"
|
"name": "python3"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user