fetch_ml/cli/src/commands/jupyter/query.zig
Jeremie Fraeys 90e5c6dc17
refactor(cli): modularize jupyter command
Break down jupyter.zig (31KB, 906 lines) into focused modules:
- jupyter/mod.zig - Main entry point and command dispatch
- jupyter/validation.zig - Security validation functions
- jupyter/lifecycle.zig - Service create/start/stop/remove/restore
- jupyter/query.zig - List, status, and package queries
- jupyter/workspace.zig - Workspace and experiment management

Original jupyter.zig now acts as backward-compatible wrapper.
Removed 5 unimplemented placeholder functions (~50 lines of dead code).

Benefits:
- Each module <250 lines (maintainable)
- Clear separation of concerns
- Easier to test individual components
- Better code organization

All tests pass.
2026-03-04 21:55:37 -05:00

255 lines
9.3 KiB
Zig

const std = @import("std");
const Config = @import("../../config.zig").Config;
const ws = @import("../../net/ws/client.zig");
const crypto = @import("../../utils/crypto.zig");
const protocol = @import("../../net/protocol.zig");
/// List Jupyter services
pub fn listJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
_ = args;
_ = json;
try listServices(allocator);
}
/// Show Jupyter service status
pub fn statusJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
_ = args;
_ = json;
// Re-use listServices for now as status is part of list
try listServices(allocator);
}
/// Internal function to list all services
fn listServices(allocator: std.mem.Allocator) !void {
const config = try Config.load(allocator);
defer {
var mut_config = config;
mut_config.deinit(allocator);
}
const url = try config.getWebSocketUrl(allocator);
defer allocator.free(url);
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
std.debug.print("Failed to connect to server: {}\n", .{err});
return;
};
defer client.close();
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
defer allocator.free(api_key_hash);
client.sendListJupyter(api_key_hash) catch |err| {
std.debug.print("Failed to send list command: {}\n", .{err});
return;
};
const response = client.receiveMessage(allocator) catch |err| {
std.debug.print("Failed to receive response: {}\n", .{err});
return;
};
defer allocator.free(response);
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
std.debug.print("Failed to parse response: {}\n", .{err});
return;
};
defer packet.deinit(allocator);
switch (packet.packet_type) {
.data => {
std.debug.print("Jupyter Services:\n", .{});
if (packet.data_payload) |payload| {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch {
std.debug.print("{s}\n", .{payload});
return;
};
defer parsed.deinit();
var services_opt: ?std.json.Array = null;
if (parsed.value == .array) {
services_opt = parsed.value.array;
} else if (parsed.value == .object) {
if (parsed.value.object.get("services")) |sv| {
if (sv == .array) services_opt = sv.array;
}
}
if (services_opt == null) {
std.debug.print("{s}\n", .{payload});
return;
}
const services = services_opt.?;
if (services.items.len == 0) {
std.debug.print("No running services.\n", .{});
return;
}
std.debug.print("NAME\t\t\t\t\t\t\t\t\t\t\tSTATUS\t\tURL\t\t\t\t\t\t\t\t\t\t\t\t\tWORKSPACE\n", .{});
std.debug.print("---- ------ --- ---------\n", .{});
for (services.items) |item| {
if (item != .object) continue;
const obj = item.object;
var name: []const u8 = "";
if (obj.get("name")) |v| {
if (v == .string) name = v.string;
}
var status: []const u8 = "";
if (obj.get("status")) |v| {
if (v == .string) status = v.string;
}
var url_str: []const u8 = "";
if (obj.get("url")) |v| {
if (v == .string) url_str = v.string;
}
var workspace: []const u8 = "";
if (obj.get("workspace")) |v| {
if (v == .string) workspace = v.string;
}
std.debug.print("{s: <20} {s: <9} {s: <25} {s}\n", .{ name, status, url_str, workspace });
}
}
},
.error_packet => {
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
std.debug.print("Failed to list services: {s}\n", .{error_msg});
if (packet.error_details) |details| {
std.debug.print("Details: {s}\n", .{details});
} else if (packet.error_message) |msg| {
std.debug.print("Details: {s}\n", .{msg});
}
},
else => {
std.debug.print("Unexpected response type\n", .{});
},
}
}
/// Package management commands
pub fn packageCommands(args: []const []const u8) !void {
if (args.len < 1) {
std.debug.print("Usage: ml jupyter package <list>\n", .{});
return;
}
const subcommand = args[0];
if (std.mem.eql(u8, subcommand, "list")) {
if (args.len < 2) {
std.debug.print("Usage: ml jupyter package list <service-name>\n", .{});
return;
}
var service_name: []const u8 = "";
if (std.mem.eql(u8, args[1], "--name") and args.len >= 3) {
service_name = args[2];
} else {
service_name = args[1];
}
if (service_name.len == 0) {
std.debug.print("Service name is required\n", .{});
return;
}
const allocator = std.heap.page_allocator;
const config = try Config.load(allocator);
defer {
var mut_config = config;
mut_config.deinit(allocator);
}
const url = try config.getWebSocketUrl(allocator);
defer allocator.free(url);
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
std.debug.print("Failed to connect to server: {}\n", .{err});
return;
};
defer client.close();
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
defer allocator.free(api_key_hash);
client.sendListJupyterPackages(service_name, api_key_hash) catch |err| {
std.debug.print("Failed to send list packages command: {}\n", .{err});
return;
};
const response = client.receiveMessage(allocator) catch |err| {
std.debug.print("Failed to receive response: {}\n", .{err});
return;
};
defer allocator.free(response);
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
std.debug.print("Failed to parse response: {}\n", .{err});
return;
};
defer packet.deinit(allocator);
switch (packet.packet_type) {
.data => {
std.debug.print("Installed packages for {s}:\n", .{service_name});
if (packet.data_payload) |payload| {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch {
std.debug.print("{s}\n", .{payload});
return;
};
defer parsed.deinit();
if (parsed.value != .array) {
std.debug.print("{s}\n", .{payload});
return;
}
const pkgs = parsed.value.array;
if (pkgs.items.len == 0) {
std.debug.print("No packages found.\n", .{});
return;
}
std.debug.print("NAME VERSION SOURCE\n", .{});
std.debug.print("---- ------- ------\n", .{});
for (pkgs.items) |item| {
if (item != .object) continue;
const obj = item.object;
var name: []const u8 = "";
if (obj.get("name")) |v| {
if (v == .string) name = v.string;
}
var version: []const u8 = "";
if (obj.get("version")) |v| {
if (v == .string) version = v.string;
}
var source: []const u8 = "";
if (obj.get("source")) |v| {
if (v == .string) source = v.string;
}
std.debug.print("{s: <30} {s: <22} {s}\n", .{ name, version, source });
}
}
},
.error_packet => {
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
std.debug.print("Failed to list packages: {s}\n", .{error_msg});
if (packet.error_details) |details| {
std.debug.print("Details: {s}\n", .{details});
} else if (packet.error_message) |msg| {
std.debug.print("Details: {s}\n", .{msg});
}
},
else => {
std.debug.print("Unexpected response type\n", .{});
},
}
} else {
std.debug.print("Invalid package command: {s}\n", .{subcommand});
}
}