-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
138 lines (118 loc) · 5.65 KB
/
cli.py
File metadata and controls
138 lines (118 loc) · 5.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from __future__ import annotations
import json
from typing import List, Optional, Annotated
import typer
from commands.config import AppConfig, load_config
from commands.analyze_kinds import analyze_kinds, print_summary_table
from commands.analyze_entity_fields import analyze_field_contributions, print_field_summary
from commands.cleanup_expired import cleanup_expired
app = typer.Typer(help="Utilities for analyzing and managing local Datastore/Firestore (Datastore mode)", no_args_is_help=True)
# Aliases with flags only — no defaults here
ConfigOpt = Annotated[Optional[str], typer.Option("--config", help="Path to config.yaml")]
ProjectOpt = Annotated[Optional[str], typer.Option("--project", help="GCP/Emulator project id")]
EmulatorHostOpt = Annotated[Optional[str], typer.Option("--emulator-host", help="Emulator host, e.g. localhost:8010")]
LogLevelOpt = Annotated[Optional[str], typer.Option("--log-level", help="Logging level")]
KindsOpt = Annotated[
Optional[List[str]],
typer.Option("--kind", "-k", help="Kinds to process (omit or empty to process all in each namespace)")
]
SingleKindOpt = Annotated[Optional[str], typer.Option("--kind", "-k", help="Kind to analyze (falls back to config.kind)")]
def _load_cfg(
config_path: Optional[str],
project: Optional[str],
emulator_host: Optional[str],
log_level: Optional[str],
) -> AppConfig:
overrides = {}
if project:
overrides["project_id"] = project
if emulator_host:
overrides["emulator_host"] = emulator_host
if log_level:
overrides["log_level"] = log_level
return load_config(config_path, overrides)
@app.command("analyze-kinds")
def cmd_analyze_kinds(
config: ConfigOpt = None,
project: ProjectOpt = None,
emulator_host: EmulatorHostOpt = None,
log_level: LogLevelOpt = None,
kind: KindsOpt = None,
output: Annotated[Optional[str], typer.Option("--output", help="Output CSV file path")] = None,
):
cfg = _load_cfg(config, project, emulator_host, log_level)
if kind is not None:
# Normalise: treat [""] as empty (all kinds)
cfg.kinds = [k for k in kind if k] # drop empty strings
rows = analyze_kinds(cfg)
if output:
with open(output, "w", encoding="utf-8") as fh:
fh.write("namespace,kind,count,size,bytes\n")
for r in rows:
ns = r.get("namespace") or ""
fh.write(f"{ns},{r['kind']},{r['count']},{r['size']},{r['bytes']}\n")
typer.echo(f"Wrote {len(rows)} rows to {output}")
else:
print_summary_table(rows)
@app.command("analyze-fields")
def cmd_analyze_fields(
kind: SingleKindOpt = None,
namespace: Annotated[Optional[str], typer.Option("--namespace", "-n", help="Namespace to query (omit to use all)")] = None,
group_by: Annotated[Optional[str], typer.Option("--group-by", help="Group results by this field value (falls back to config.group_by_field)")] = None,
only_field: Annotated[Optional[List[str]], typer.Option("--only-field", help="Only consider these fields")] = None,
config: ConfigOpt = None,
project: ProjectOpt = None,
emulator_host: EmulatorHostOpt = None,
log_level: LogLevelOpt = None,
output_json: Annotated[Optional[str], typer.Option("--output-json", help="Write raw JSON results to file")] = None,
):
cfg = _load_cfg(config, project, emulator_host, log_level)
target_kind = kind or cfg.kind
target_namespace = namespace if namespace is not None else cfg.namespace
group_by_field = group_by if group_by is not None else cfg.group_by_field
if not target_kind:
raise typer.BadParameter("--kind is required (either via flag or config.kind)")
result = analyze_field_contributions(
cfg,
kind=target_kind,
namespace=target_namespace,
group_by_field=group_by_field,
only_fields=[f for f in only_field] if only_field else None,
)
if output_json:
with open(output_json, "w", encoding="utf-8") as fh:
json.dump(result, fh, indent=2)
typer.echo(f"Wrote JSON results to {output_json}")
else:
print_field_summary(result)
@app.command("cleanup")
def cmd_cleanup(
config: ConfigOpt = None,
project: ProjectOpt = None,
emulator_host: EmulatorHostOpt = None,
log_level: LogLevelOpt = None,
kind: KindsOpt = None,
ttl_field: Annotated[Optional[str], typer.Option("--ttl-field", help="TTL field name (falls back to config.ttl_field)")] = None,
delete_missing_ttl: Annotated[Optional[bool], typer.Option("--delete-missing-ttl", help="Delete when TTL field is missing (falls back to config.delete_missing_ttl)")] = None,
batch_size: Annotated[Optional[int], typer.Option("--batch-size", help="Delete batch size (falls back to config.batch_size)")] = None,
dry_run: Annotated[bool, typer.Option("--dry-run", help="Only report counts; do not delete")] = False,
):
cfg = _load_cfg(config, project, emulator_host, log_level)
if kind is not None:
cfg.kinds = [k for k in kind if k]
if ttl_field is not None:
cfg.ttl_field = ttl_field
if delete_missing_ttl is not None:
cfg.delete_missing_ttl = delete_missing_ttl
if batch_size is not None:
cfg.batch_size = batch_size
totals = cleanup_expired(cfg, dry_run=dry_run)
deleted_sum = sum(totals.values())
typer.echo(f"Total entities {'to delete' if dry_run else 'deleted'}: {deleted_sum}")
if __name__ == "__main__":
import sys
# If invoked with no subcommand/arguments, show help (list available commands/options)
if len(sys.argv) == 1:
# append --help so Typer/Click prints the global help instead of raising Missing command
sys.argv.append("--help")
app()