Skip to content

Commit cfe69fb

Browse files
author
Douglas Blank
committed
Version 3.1.1
1 parent f3f06f1 commit cfe69fb

File tree

3 files changed

+243
-5
lines changed

3 files changed

+243
-5
lines changed

cometx/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
# the express permission of Comet ML Inc.
1313
# *******************************************************
1414

15-
version_info = (3, 1, 0)
15+
version_info = (3, 1, 1)
1616
__version__ = ".".join(map(str, version_info))

cometx/cli/admin.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
To perform admin functions
1616
1717
cometx admin chargeback-report
18+
cometx admin usage-report WORKSPACE/PROJECT
1819
1920
"""
2021

@@ -23,13 +24,17 @@
2324
import sys
2425
from urllib.parse import urlparse
2526

27+
from comet_ml import API
28+
29+
from .admin_usage_report import generate_usage_report
30+
2631
ADDITIONAL_ARGS = False
2732

2833

2934
def get_parser_arguments(parser):
3035
parser.add_argument(
3136
"ACTION",
32-
help="The admin action to perform (chargeback-report)",
37+
help="The admin action to perform (chargeback-report, usage-report)",
3338
type=str,
3439
)
3540
parser.add_argument(
@@ -40,6 +45,13 @@ def get_parser_arguments(parser):
4045
type=str,
4146
default=None,
4247
)
48+
parser.add_argument(
49+
"WORKSPACE_PROJECT",
50+
nargs="?",
51+
help="The WORKSPACE/PROJECT to run usage report for (required for usage-report action)",
52+
type=str,
53+
default=None,
54+
)
4355
parser.add_argument(
4456
"--host",
4557
help="Override the HOST URL",
@@ -48,12 +60,16 @@ def get_parser_arguments(parser):
4860
parser.add_argument(
4961
"--debug", help="If given, allow debugging", default=False, action="store_true"
5062
)
63+
parser.add_argument(
64+
"--no-open",
65+
help="Don't automatically open the generated PDF file",
66+
default=False,
67+
action="store_true",
68+
)
5169

5270

5371
def admin(parsed_args, remaining=None):
5472
# Called via `cometx admin ...`
55-
from comet_ml import API
56-
5773
try:
5874
api = API()
5975

@@ -93,9 +109,22 @@ def admin(parsed_args, remaining=None):
93109
with open(filename, "w") as fp:
94110
fp.write(json.dumps(response.json()))
95111
print("Chargeback report is saved in %r" % filename)
112+
elif parsed_args.ACTION == "usage-report":
113+
# For usage-report, the workspace/project is passed as YEAR_MONTH argument
114+
workspace_project = parsed_args.YEAR_MONTH or parsed_args.WORKSPACE_PROJECT
115+
if not workspace_project:
116+
print("ERROR: WORKSPACE/PROJECT is required for usage-report action")
117+
print("Usage: cometx admin usage-report WORKSPACE/PROJECT")
118+
return
119+
try:
120+
generate_usage_report(api, workspace_project, parsed_args.no_open)
121+
122+
except Exception as e:
123+
print("ERROR: " + str(e))
124+
return
96125
else:
97126
print(
98-
"Unknown action %r; should be one of these: 'chargeback-report'"
127+
"Unknown action %r; should be one of these: 'chargeback-report', 'usage-report'"
99128
% parsed_args.ACTION
100129
)
101130

cometx/cli/admin_usage_report.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# ****************************************
4+
# __
5+
# _________ ____ ___ ___ / /__ __
6+
# / ___/ __ \/ __ `__ \/ _ \/ __/ |/_/
7+
# / /__/ /_/ / / / / / / __/ /__> <
8+
# \___/\____/_/ /_/ /_/\___/\__/_/|_|
9+
#
10+
#
11+
# Copyright (c) 2024 Cometx Development
12+
# Team. All rights reserved.
13+
# ****************************************
14+
"""
15+
Usage report functionality for admin commands
16+
17+
cometx admin usage-report WORKSPACE/PROJECT
18+
19+
"""
20+
21+
import warnings
22+
from collections import defaultdict
23+
from datetime import datetime
24+
25+
import matplotlib.pyplot as plt
26+
from comet_ml import API
27+
28+
# Suppress matplotlib warnings about non-GUI backend
29+
warnings.filterwarnings("ignore", category=UserWarning, module="matplotlib")
30+
31+
32+
def open_pdf(pdf_path):
33+
"""Open PDF file using the default system application"""
34+
import os
35+
import webbrowser
36+
37+
if os.path.exists(pdf_path):
38+
webbrowser.open(f"file://{os.path.abspath(pdf_path)}")
39+
print(f"Opening PDF: {pdf_path}")
40+
else:
41+
print(f"PDF file not found: {pdf_path}")
42+
43+
44+
def generate_usage_report(api, workspace_project, no_open=False):
45+
"""
46+
Get usage report for a specific workspace/project.
47+
48+
Args:
49+
api: Comet API instance
50+
workspace_project: String in format "workspace/project"
51+
no_open: If True, don't automatically open the generated PDF file
52+
53+
Returns:
54+
dict: The usage report data
55+
"""
56+
if not workspace_project:
57+
print("ERROR: workspace_project is required")
58+
return {}
59+
60+
if "/" in workspace_project:
61+
workspace, project = workspace_project.split("/")
62+
else:
63+
workspace = workspace_project
64+
project = None
65+
66+
api = API()
67+
# Get project experiments data
68+
print(f"Fetching experiments for workspace: {workspace}, project: {project}")
69+
project_data = api._client.get_project_experiments(workspace, project)
70+
71+
if not project_data:
72+
print(f"API returned None for {workspace_project}")
73+
return {}
74+
75+
if not isinstance(project_data, dict):
76+
print(
77+
f"API returned unexpected data type: {type(project_data)} for {workspace_project}"
78+
)
79+
return {}
80+
81+
if "experiments" not in project_data:
82+
print(f"No 'experiments' key in API response for {workspace_project}")
83+
print(
84+
f"Available keys: {list(project_data.keys()) if project_data else 'None'}"
85+
)
86+
return {}
87+
88+
experiments = project_data["experiments"]
89+
print(f"Found {len(experiments)} experiments for {workspace_project}")
90+
91+
# Group experiments by month/year based on startTimeMillis
92+
monthly_counts = defaultdict(int)
93+
94+
for exp in experiments:
95+
if "startTimeMillis" in exp and exp["startTimeMillis"]:
96+
# Convert milliseconds to seconds, then to datetime
97+
start_time = datetime.fromtimestamp(exp["startTimeMillis"] / 1000)
98+
# Format as YYYY-MM for grouping
99+
month_key = start_time.strftime("%Y-%m")
100+
monthly_counts[month_key] += 1
101+
102+
if not monthly_counts:
103+
print("No experiments with valid start times found")
104+
return {}
105+
106+
# Create a complete range of months from first to last experiment
107+
all_months = sorted(monthly_counts.keys())
108+
if not all_months:
109+
print("No valid months found")
110+
return {}
111+
112+
# Generate all months between first and last
113+
start_date = datetime.strptime(all_months[0], "%Y-%m")
114+
end_date = datetime.strptime(all_months[-1], "%Y-%m")
115+
116+
complete_months = []
117+
current_date = start_date
118+
while current_date <= end_date:
119+
month_key = current_date.strftime("%Y-%m")
120+
complete_months.append(month_key)
121+
# Move to next month
122+
if current_date.month == 12:
123+
current_date = current_date.replace(year=current_date.year + 1, month=1)
124+
else:
125+
current_date = current_date.replace(month=current_date.month + 1)
126+
127+
# Fill in counts for all months (0 for months with no experiments)
128+
counts = [monthly_counts[month] for month in complete_months]
129+
130+
# Create bar chart with complete timeline
131+
plt.figure(figsize=(14, 8))
132+
133+
# Create bars with different colors for zero vs non-zero values
134+
bar_colors = ["lightcoral" if count == 0 else "steelblue" for count in counts]
135+
bars = plt.bar(
136+
range(len(complete_months)),
137+
counts,
138+
color=bar_colors,
139+
edgecolor="navy",
140+
alpha=0.7,
141+
width=0.8,
142+
)
143+
144+
# Customize the chart
145+
plt.title(
146+
f"Experiment Count by Month - {workspace_project}",
147+
fontsize=16,
148+
fontweight="bold",
149+
pad=20,
150+
)
151+
plt.xlabel("Month", fontsize=14)
152+
plt.ylabel("Number of Experiments", fontsize=14)
153+
154+
# Set x-axis labels - show every 3rd month to avoid crowding
155+
step = max(1, len(complete_months) // 20) # Show about 20 labels max
156+
x_ticks = range(0, len(complete_months), step)
157+
x_labels = [complete_months[i] for i in x_ticks]
158+
plt.xticks(x_ticks, x_labels, rotation=45, ha="right")
159+
160+
# Add grid for better readability
161+
plt.grid(True, alpha=0.3, linestyle="-", linewidth=0.5)
162+
163+
# Add value labels on bars (only for non-zero values to avoid clutter)
164+
for i, (bar, count) in enumerate(zip(bars, counts)):
165+
if count > 0: # Only label non-zero bars
166+
plt.text(
167+
bar.get_x() + bar.get_width() / 2,
168+
bar.get_height() + 0.1,
169+
str(count),
170+
ha="center",
171+
va="bottom",
172+
fontsize=9,
173+
fontweight="bold",
174+
)
175+
176+
# Adjust layout to prevent label cutoff
177+
plt.tight_layout()
178+
179+
# Save the chart as PNG
180+
png_filename = f"experiment_usage_report_{workspace_project.replace('/', '_')}.png"
181+
plt.savefig(png_filename, dpi=300, bbox_inches="tight")
182+
183+
# Save the chart as PDF
184+
pdf_filename = f"experiment_usage_report_{workspace_project.replace('/', '_')}.pdf"
185+
plt.savefig(pdf_filename, format="pdf", bbox_inches="tight")
186+
187+
# Display summary statistics
188+
print(f"\nSummary for {workspace_project}:")
189+
print(f"Total experiments: {sum(counts)}")
190+
print(f"Date range: {complete_months[0]} to {complete_months[-1]}")
191+
print(f"Months with experiments: {sum(1 for c in counts if c > 0)}")
192+
print(f"Months with zero experiments: {sum(1 for c in counts if c == 0)}")
193+
print(f"Average experiments per month: {sum(counts)/len(counts):.1f}")
194+
195+
# Show file save message after summary
196+
print(f"Bar chart saved as: {pdf_filename}")
197+
198+
# Open PDF file unless --no-open flag is set
199+
if not no_open:
200+
open_pdf(pdf_filename)
201+
202+
return {
203+
"total_experiments": sum(counts),
204+
"monthly_counts": dict(monthly_counts),
205+
"complete_monthly_counts": dict(zip(complete_months, counts)),
206+
"date_range": (complete_months[0], complete_months[-1]),
207+
"png_file": png_filename,
208+
"pdf_file": pdf_filename,
209+
}

0 commit comments

Comments
 (0)