clan-app: Implement dynamic log groups into javascript callApi
nix fmt
This commit is contained in:
@@ -11,7 +11,7 @@ import clan_lib.machines.actions # noqa: F401
|
|||||||
from clan_lib.api import API, load_in_all_api_functions, tasks
|
from clan_lib.api import API, load_in_all_api_functions, tasks
|
||||||
from clan_lib.custom_logger import setup_logging
|
from clan_lib.custom_logger import setup_logging
|
||||||
from clan_lib.dirs import user_data_dir
|
from clan_lib.dirs import user_data_dir
|
||||||
from clan_lib.log_manager import LogManager
|
from clan_lib.log_manager import LogGroupConfig, LogManager
|
||||||
from clan_lib.log_manager import api as log_manager_api
|
from clan_lib.log_manager import api as log_manager_api
|
||||||
|
|
||||||
from clan_app.api.file_gtk import open_file
|
from clan_app.api.file_gtk import open_file
|
||||||
@@ -41,12 +41,15 @@ def app_run(app_opts: ClanAppOptions) -> int:
|
|||||||
|
|
||||||
webview = Webview(debug=app_opts.debug)
|
webview = Webview(debug=app_opts.debug)
|
||||||
webview.title = "Clan App"
|
webview.title = "Clan App"
|
||||||
# This seems to call the gtk api correctly but and gtk also seems to our icon, but somehow the icon is not loaded.
|
|
||||||
|
|
||||||
# Init LogManager global in log_manager_api module
|
# Add a log group ["clans", <dynamic_name>, "machines", <dynamic_name>]
|
||||||
log_manager_api.LOG_MANAGER_INSTANCE = LogManager(
|
log_manager = LogManager(base_dir=user_data_dir() / "clan-app" / "logs")
|
||||||
base_dir=user_data_dir() / "clan-app" / "logs"
|
clan_log_group = LogGroupConfig("clans", "Clans").add_child(
|
||||||
|
LogGroupConfig("machines", "Machines")
|
||||||
)
|
)
|
||||||
|
log_manager = log_manager.add_root_group_config(clan_log_group)
|
||||||
|
# Init LogManager global in log_manager_api module
|
||||||
|
log_manager_api.LOG_MANAGER_INSTANCE = log_manager
|
||||||
|
|
||||||
# Init BAKEND_THREADS global in tasks module
|
# Init BAKEND_THREADS global in tasks module
|
||||||
tasks.BAKEND_THREADS = webview.threads
|
tasks.BAKEND_THREADS = webview.threads
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# ruff: noqa: TRY301
|
||||||
import functools
|
import functools
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
@@ -66,15 +67,24 @@ class Webview:
|
|||||||
) -> None:
|
) -> None:
|
||||||
op_key = op_key_bytes.decode()
|
op_key = op_key_bytes.decode()
|
||||||
args = json.loads(request_data.decode())
|
args = json.loads(request_data.decode())
|
||||||
log.debug(f"Calling {method_name}({args})")
|
log.debug(f"Calling {method_name}({json.dumps(args, indent=4)})")
|
||||||
header: dict[str, Any]
|
header: dict[str, Any]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Initialize dataclasses from the payload
|
# Initialize dataclasses from the payload
|
||||||
reconciled_arguments = {}
|
reconciled_arguments = {}
|
||||||
if len(args) > 1:
|
if len(args) == 1:
|
||||||
header = args[1]
|
request = args[0]
|
||||||
for k, v in args[0].items():
|
header = request.get("header", {})
|
||||||
|
msg = f"Expected header to be a dict, got {type(header)}"
|
||||||
|
if not isinstance(header, dict):
|
||||||
|
raise TypeError(msg)
|
||||||
|
body = request.get("body", {})
|
||||||
|
msg = f"Expected body to be a dict, got {type(body)}"
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
for k, v in body.items():
|
||||||
# Some functions expect to be called with dataclass instances
|
# Some functions expect to be called with dataclass instances
|
||||||
# But the js api returns dictionaries.
|
# But the js api returns dictionaries.
|
||||||
# Introspect the function and create the expected dataclass from dict dynamically
|
# Introspect the function and create the expected dataclass from dict dynamically
|
||||||
@@ -84,8 +94,11 @@ class Webview:
|
|||||||
# TODO: rename from_dict into something like construct_checked_value
|
# TODO: rename from_dict into something like construct_checked_value
|
||||||
# from_dict really takes Anything and returns an instance of the type/class
|
# from_dict really takes Anything and returns an instance of the type/class
|
||||||
reconciled_arguments[k] = from_dict(arg_class, v)
|
reconciled_arguments[k] = from_dict(arg_class, v)
|
||||||
elif len(args) == 1:
|
elif len(args) > 1:
|
||||||
header = args[0]
|
msg = (
|
||||||
|
"Expected a single argument, got multiple arguments to api_wrapper"
|
||||||
|
)
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
reconciled_arguments["op_key"] = op_key
|
reconciled_arguments["op_key"] = op_key
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -114,11 +127,11 @@ class Webview:
|
|||||||
try:
|
try:
|
||||||
# If the API call has set log_group in metadata,
|
# If the API call has set log_group in metadata,
|
||||||
# create the log file under that group.
|
# create the log file under that group.
|
||||||
log_group: list[str] = header.get("logging", {}).get("group", None)
|
log_group: list[str] = header.get("logging", {}).get("group_path", None)
|
||||||
if log_group is not None:
|
if log_group is not None:
|
||||||
if not isinstance(log_group, list):
|
if not isinstance(log_group, list):
|
||||||
msg = f"Expected log_group to be a list, got {type(log_group)}"
|
msg = f"Expected log_group to be a list, got {type(log_group)}"
|
||||||
raise TypeError(msg) # noqa: TRY301
|
raise TypeError(msg)
|
||||||
log.warning(
|
log.warning(
|
||||||
f"Using log group {log_group} for {method_name} with op_key {op_key}"
|
f"Using log group {log_group} for {method_name} with op_key {op_key}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,42 +23,25 @@ export type SuccessQuery<T extends OperationNames> = Extract<
|
|||||||
>;
|
>;
|
||||||
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
||||||
|
|
||||||
function isMachine(obj: unknown): obj is Machine {
|
interface SendHeaderType {
|
||||||
return (
|
logging?: { group_path: string[] };
|
||||||
!!obj &&
|
}
|
||||||
typeof obj === "object" &&
|
interface BackendSendType<K extends OperationNames> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
body: OperationArgs<K>;
|
||||||
typeof (obj as any).name === "string" &&
|
header?: SendHeaderType;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
typeof (obj as any).flake === "object" &&
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
typeof (obj as any).flake.identifier === "string"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Machine type with flake for API calls
|
|
||||||
interface Machine {
|
|
||||||
name: string;
|
|
||||||
flake: {
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BackendOpts {
|
|
||||||
logging?: { group: string | Machine };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
interface ReceiveHeaderType {}
|
||||||
interface BackendReturnType<K extends OperationNames> {
|
interface BackendReturnType<K extends OperationNames> {
|
||||||
body: OperationResponse<K>;
|
body: OperationResponse<K>;
|
||||||
|
header: ReceiveHeaderType;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
header: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const _callApi = <K extends OperationNames>(
|
const _callApi = <K extends OperationNames>(
|
||||||
method: K,
|
method: K,
|
||||||
args: OperationArgs<K>,
|
args: OperationArgs<K>,
|
||||||
backendOpts?: BackendOpts,
|
backendOpts?: SendHeaderType,
|
||||||
): { promise: Promise<BackendReturnType<K>>; op_key: string } => {
|
): { promise: Promise<BackendReturnType<K>>; op_key: string } => {
|
||||||
// if window[method] does not exist, throw an error
|
// if window[method] does not exist, throw an error
|
||||||
if (!(method in window)) {
|
if (!(method in window)) {
|
||||||
@@ -82,26 +65,19 @@ const _callApi = <K extends OperationNames>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let header: BackendOpts = {};
|
const message: BackendSendType<OperationNames> = {
|
||||||
if (backendOpts != undefined) {
|
body: args,
|
||||||
header = { ...backendOpts };
|
header: backendOpts,
|
||||||
const group = backendOpts?.logging?.group;
|
|
||||||
if (group != undefined && isMachine(group)) {
|
|
||||||
header = {
|
|
||||||
logging: { group: group.flake.identifier + "#" + group.name },
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const promise = (
|
const promise = (
|
||||||
window as unknown as Record<
|
window as unknown as Record<
|
||||||
OperationNames,
|
OperationNames,
|
||||||
(
|
(
|
||||||
args: OperationArgs<OperationNames>,
|
args: BackendSendType<OperationNames>,
|
||||||
metadata: BackendOpts,
|
|
||||||
) => Promise<BackendReturnType<OperationNames>>
|
) => Promise<BackendReturnType<OperationNames>>
|
||||||
>
|
>
|
||||||
)[method](args, header) as Promise<BackendReturnType<K>>;
|
)[method](message) as Promise<BackendReturnType<K>>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const op_key = (promise as any)._webviewMessageId as string;
|
const op_key = (promise as any)._webviewMessageId as string;
|
||||||
@@ -153,7 +129,7 @@ const handleCancel = async <K extends OperationNames>(
|
|||||||
export const callApi = <K extends OperationNames>(
|
export const callApi = <K extends OperationNames>(
|
||||||
method: K,
|
method: K,
|
||||||
args: OperationArgs<K>,
|
args: OperationArgs<K>,
|
||||||
backendOpts?: BackendOpts,
|
backendOpts?: SendHeaderType,
|
||||||
): { promise: Promise<OperationResponse<K>>; op_key: string } => {
|
): { promise: Promise<OperationResponse<K>>; op_key: string } => {
|
||||||
console.log("Calling API", method, args, backendOpts);
|
console.log("Calling API", method, args, backendOpts);
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ export function RemoteForm(props: RemoteFormProps) {
|
|||||||
props.queryFn,
|
props.queryFn,
|
||||||
props.machine?.name,
|
props.machine?.name,
|
||||||
props.machine?.flake,
|
props.machine?.flake,
|
||||||
|
props.machine?.flake.identifier,
|
||||||
props.field || "targetHost",
|
props.field || "targetHost",
|
||||||
],
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -209,7 +210,12 @@ export function RemoteForm(props: RemoteFormProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
logging: {
|
logging: {
|
||||||
group: { name: props.machine.name, flake: props.machine.flake },
|
group_path: [
|
||||||
|
"clans",
|
||||||
|
props.machine.flake.identifier,
|
||||||
|
"machines",
|
||||||
|
props.machine.name,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
).promise;
|
).promise;
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
|||||||
flake: { identifier: active_clan },
|
flake: { identifier: active_clan },
|
||||||
name: name,
|
name: name,
|
||||||
},
|
},
|
||||||
{ logging: { group: { name, flake: { identifier: active_clan } } } },
|
{
|
||||||
|
logging: { group_path: ["clans", active_clan, "machines", name] },
|
||||||
|
},
|
||||||
).promise;
|
).promise;
|
||||||
|
|
||||||
if (target_host.status == "error") {
|
if (target_host.status == "error") {
|
||||||
@@ -115,7 +117,9 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
|||||||
name: name,
|
name: name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
logging: { group: { name, flake: { identifier: active_clan } } },
|
logging: {
|
||||||
|
group_path: ["clans", active_clan, "machines", name],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
).promise;
|
).promise;
|
||||||
|
|
||||||
@@ -141,7 +145,11 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
|||||||
flake: { identifier: active_clan },
|
flake: { identifier: active_clan },
|
||||||
name: name,
|
name: name,
|
||||||
},
|
},
|
||||||
{ logging: { group: { name, flake: { identifier: active_clan } } } },
|
{
|
||||||
|
logging: {
|
||||||
|
group_path: ["clans", active_clan, "machines", name],
|
||||||
|
},
|
||||||
|
},
|
||||||
).promise;
|
).promise;
|
||||||
|
|
||||||
if (build_host.status == "error") {
|
if (build_host.status == "error") {
|
||||||
@@ -166,7 +174,11 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
|||||||
target_host: target_host.data!.data,
|
target_host: target_host.data!.data,
|
||||||
build_host: build_host.data?.data || null,
|
build_host: build_host.data?.data || null,
|
||||||
},
|
},
|
||||||
{ logging: { group: { name, flake: { identifier: active_clan } } } },
|
{
|
||||||
|
logging: {
|
||||||
|
group_path: ["clans", active_clan, "machines", name],
|
||||||
|
},
|
||||||
|
},
|
||||||
).promise;
|
).promise;
|
||||||
|
|
||||||
setUpdating(false);
|
setUpdating(false);
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function MachineForm(props: MachineFormProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
logging: {
|
logging: {
|
||||||
group: { name: machine_name, flake: { identifier: base_dir } },
|
group_path: ["clans", base_dir, "machines", machine_name],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
).promise;
|
).promise;
|
||||||
@@ -130,7 +130,9 @@ export function MachineForm(props: MachineFormProps) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
logging: { group: { name: machine, flake: { identifier: curr_uri } } },
|
logging: {
|
||||||
|
group_path: ["clans", curr_uri, "machines", machine],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
).promise;
|
).promise;
|
||||||
|
|
||||||
@@ -161,7 +163,9 @@ export function MachineForm(props: MachineFormProps) {
|
|||||||
build_host: null,
|
build_host: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
logging: { group: { name: machine, flake: { identifier: curr_uri } } },
|
logging: {
|
||||||
|
group_path: ["clans", curr_uri, "machines", machine],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
).promise.finally(() => {
|
).promise.finally(() => {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const VarsStep = (props: VarsStepProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
logging: {
|
logging: {
|
||||||
group: { name: props.machine_id, flake: { identifier: props.dir } },
|
group_path: ["clans", props.dir, "machines", props.machine_id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
).promise;
|
).promise;
|
||||||
|
|||||||
@@ -16,14 +16,22 @@ export const MachineInstall = () => {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const curr = activeClanURI();
|
const curr = activeClanURI();
|
||||||
if (curr) {
|
if (curr) {
|
||||||
const result = await callApi("get_machine_details", {
|
const result = await callApi(
|
||||||
|
"get_machine_details",
|
||||||
|
{
|
||||||
machine: {
|
machine: {
|
||||||
flake: {
|
flake: {
|
||||||
identifier: curr,
|
identifier: curr,
|
||||||
},
|
},
|
||||||
name: params.id,
|
name: params.id,
|
||||||
},
|
},
|
||||||
}).promise;
|
},
|
||||||
|
{
|
||||||
|
logging: {
|
||||||
|
group_path: ["clans", curr, "machines", params.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).promise;
|
||||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import Icon from "@/src/components/icon";
|
|||||||
import { Header } from "@/src/layout/header";
|
import { Header } from "@/src/layout/header";
|
||||||
import { makePersisted } from "@solid-primitives/storage";
|
import { makePersisted } from "@solid-primitives/storage";
|
||||||
import { useClanContext } from "@/src/contexts/clan";
|
import { useClanContext } from "@/src/contexts/clan";
|
||||||
import { debug } from "console";
|
|
||||||
|
|
||||||
type MachinesModel = Extract<
|
type MachinesModel = Extract<
|
||||||
OperationResponse<"list_machines">,
|
OperationResponse<"list_machines">,
|
||||||
|
|||||||
@@ -23,42 +23,25 @@ export type SuccessQuery<T extends OperationNames> = Extract<
|
|||||||
>;
|
>;
|
||||||
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
||||||
|
|
||||||
function isMachine(obj: unknown): obj is Machine {
|
interface SendHeaderType {
|
||||||
return (
|
logging?: { group_path: string[] };
|
||||||
!!obj &&
|
}
|
||||||
typeof obj === "object" &&
|
interface BackendSendType<K extends OperationNames> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
body: OperationArgs<K>;
|
||||||
typeof (obj as any).name === "string" &&
|
header?: SendHeaderType;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
typeof (obj as any).flake === "object" &&
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
typeof (obj as any).flake.identifier === "string"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Machine type with flake for API calls
|
|
||||||
interface Machine {
|
|
||||||
name: string;
|
|
||||||
flake: {
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BackendOpts {
|
|
||||||
logging?: { group: string | Machine };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
interface ReceiveHeaderType {}
|
||||||
interface BackendReturnType<K extends OperationNames> {
|
interface BackendReturnType<K extends OperationNames> {
|
||||||
body: OperationResponse<K>;
|
body: OperationResponse<K>;
|
||||||
|
header: ReceiveHeaderType;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
header: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const _callApi = <K extends OperationNames>(
|
const _callApi = <K extends OperationNames>(
|
||||||
method: K,
|
method: K,
|
||||||
args: OperationArgs<K>,
|
args: OperationArgs<K>,
|
||||||
backendOpts?: BackendOpts,
|
backendOpts?: SendHeaderType,
|
||||||
): { promise: Promise<BackendReturnType<K>>; op_key: string } => {
|
): { promise: Promise<BackendReturnType<K>>; op_key: string } => {
|
||||||
// if window[method] does not exist, throw an error
|
// if window[method] does not exist, throw an error
|
||||||
if (!(method in window)) {
|
if (!(method in window)) {
|
||||||
@@ -82,26 +65,19 @@ const _callApi = <K extends OperationNames>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let header: BackendOpts = {};
|
const message: BackendSendType<OperationNames> = {
|
||||||
if (backendOpts != undefined) {
|
body: args,
|
||||||
header = { ...backendOpts };
|
header: backendOpts,
|
||||||
const group = backendOpts?.logging?.group;
|
|
||||||
if (group != undefined && isMachine(group)) {
|
|
||||||
header = {
|
|
||||||
logging: { group: group.flake.identifier + "#" + group.name },
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const promise = (
|
const promise = (
|
||||||
window as unknown as Record<
|
window as unknown as Record<
|
||||||
OperationNames,
|
OperationNames,
|
||||||
(
|
(
|
||||||
args: OperationArgs<OperationNames>,
|
args: BackendSendType<OperationNames>,
|
||||||
metadata: BackendOpts,
|
|
||||||
) => Promise<BackendReturnType<OperationNames>>
|
) => Promise<BackendReturnType<OperationNames>>
|
||||||
>
|
>
|
||||||
)[method](args, header) as Promise<BackendReturnType<K>>;
|
)[method](message) as Promise<BackendReturnType<K>>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const op_key = (promise as any)._webviewMessageId as string;
|
const op_key = (promise as any)._webviewMessageId as string;
|
||||||
@@ -153,7 +129,7 @@ const handleCancel = async <K extends OperationNames>(
|
|||||||
export const callApi = <K extends OperationNames>(
|
export const callApi = <K extends OperationNames>(
|
||||||
method: K,
|
method: K,
|
||||||
args: OperationArgs<K>,
|
args: OperationArgs<K>,
|
||||||
backendOpts?: BackendOpts,
|
backendOpts?: SendHeaderType,
|
||||||
): { promise: Promise<OperationResponse<K>>; op_key: string } => {
|
): { promise: Promise<OperationResponse<K>>; op_key: string } => {
|
||||||
console.log("Calling API", method, args, backendOpts);
|
console.log("Calling API", method, args, backendOpts);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ def list_log_days() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def list_log_groups(selector: list[str], date_day: str | None = None) -> list[str]:
|
def list_log_groups(
|
||||||
|
selector: list[str] | None, date_day: str | None = None
|
||||||
|
) -> list[str]:
|
||||||
"""List all log groups at the specified hierarchical path.
|
"""List all log groups at the specified hierarchical path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
Reference in New Issue
Block a user