Skip to content

Commit 378ff3c

Browse files
Using list of namespaces
1 parent 23cf174 commit 378ff3c

File tree

4 files changed

+30
-37
lines changed

4 files changed

+30
-37
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ kinds: [] # Empty -> iterate all kinds per namespace
4949

5050
# Optional defaults
5151
kind: "SourceCollectionStateEntity" # Default for analyze-fields
52-
namespace: "" # Default namespace for analyze-fields
5352

5453
# Cleanup
5554
ttl_field: "expireAt"

cli.py

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,12 @@
1717
ProjectOpt = Annotated[Optional[str], typer.Option("--project", help="GCP/Emulator project id")]
1818
EmulatorHostOpt = Annotated[Optional[str], typer.Option("--emulator-host", help="Emulator host, e.g. localhost:8010")]
1919
LogLevelOpt = Annotated[Optional[str], typer.Option("--log-level", help="Logging level")]
20-
NamespacesOpt = Annotated[
21-
Optional[List[str]],
22-
typer.Option("--namespace", "-n", help="Namespaces to process (omit to process all)")
23-
]
2420
KindsOpt = Annotated[
2521
Optional[List[str]],
26-
typer.Option("--kind", "-k", help="Kinds to process (omit to process all in each namespace)")
22+
typer.Option("--kind", "-k", help="Kinds to process (omit or empty to process all in each namespace)")
2723
]
28-
SingleNamespaceOpt = Annotated[Optional[str], typer.Option("--namespace", "-n", help="Namespace to query (omit to use all)")]
2924
SingleKindOpt = Annotated[Optional[str], typer.Option("--kind", "-k", help="Kind to analyze (falls back to config.kind)")]
3025

31-
3226
def _load_cfg(
3327
config_path: Optional[str],
3428
project: Optional[str],
@@ -44,25 +38,22 @@ def _load_cfg(
4438
overrides["log_level"] = log_level
4539
return load_config(config_path, overrides)
4640

47-
4841
@app.command("analyze-kinds")
4942
def cmd_analyze_kinds(
5043
config: ConfigOpt = None,
5144
project: ProjectOpt = None,
5245
emulator_host: EmulatorHostOpt = None,
5346
log_level: LogLevelOpt = None,
54-
namespace: NamespacesOpt = None,
5547
kind: KindsOpt = None,
5648
output: Annotated[Optional[str], typer.Option("--output", help="Output CSV file path")] = None,
5749
):
5850
cfg = _load_cfg(config, project, emulator_host, log_level)
5951

60-
if namespace:
61-
cfg.namespaces = list(namespace)
62-
if kind:
63-
cfg.kinds = list(kind)
64-
52+
if kind is not None:
53+
# Normalise: treat [""] as empty (all kinds)
54+
cfg.kinds = [k for k in kind if k] # drop empty strings
6555
rows = analyze_kinds(cfg)
56+
6657
if output:
6758
with open(output, "w", encoding="utf-8") as fh:
6859
fh.write("namespace,kind,count,size,bytes\n")
@@ -73,11 +64,10 @@ def cmd_analyze_kinds(
7364
else:
7465
print_summary_table(rows)
7566

76-
7767
@app.command("analyze-fields")
7868
def cmd_analyze_fields(
7969
kind: SingleKindOpt = None,
80-
namespace: SingleNamespaceOpt = None,
70+
namespace: Annotated[Optional[str], typer.Option("--namespace", "-n", help="Namespace to query (omit to use all)")] = None,
8171
group_by: Annotated[Optional[str], typer.Option("--group-by", help="Group results by this field value (falls back to config.group_by_field)")] = None,
8272
only_field: Annotated[Optional[List[str]], typer.Option("--only-field", help="Only consider these fields")] = None,
8373
config: ConfigOpt = None,
@@ -100,7 +90,7 @@ def cmd_analyze_fields(
10090
kind=target_kind,
10191
namespace=target_namespace,
10292
group_by_field=group_by_field,
103-
only_fields=list(only_field) if only_field else None,
93+
only_fields=[f for f in only_field] if only_field else None,
10494
)
10595

10696
if output_json:
@@ -110,14 +100,12 @@ def cmd_analyze_fields(
110100
else:
111101
print_field_summary(result)
112102

113-
114103
@app.command("cleanup")
115104
def cmd_cleanup(
116105
config: ConfigOpt = None,
117106
project: ProjectOpt = None,
118107
emulator_host: EmulatorHostOpt = None,
119108
log_level: LogLevelOpt = None,
120-
namespace: NamespacesOpt = None,
121109
kind: KindsOpt = None,
122110
ttl_field: Annotated[Optional[str], typer.Option("--ttl-field", help="TTL field name (falls back to config.ttl_field)")] = None,
123111
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,
@@ -126,10 +114,8 @@ def cmd_cleanup(
126114
):
127115
cfg = _load_cfg(config, project, emulator_host, log_level)
128116

129-
if namespace:
130-
cfg.namespaces = list(namespace)
131-
if kind:
132-
cfg.kinds = list(kind)
117+
if kind is not None:
118+
cfg.kinds = [k for k in kind if k]
133119
if ttl_field is not None:
134120
cfg.ttl_field = ttl_field
135121
if delete_missing_ttl is not None:
@@ -141,6 +127,5 @@ def cmd_cleanup(
141127
deleted_sum = sum(totals.values())
142128
typer.echo(f"Total entities {'to delete' if dry_run else 'deleted'}: {deleted_sum}")
143129

144-
145130
if __name__ == "__main__":
146131
app()

gcd_tools/analyze_kinds.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@
1414
format_size,
1515
)
1616

17-
1817
logger = logging.getLogger(__name__)
1918

20-
2119
def estimate_entity_count_and_size(
2220
client: datastore.Client, kind: str, namespace: Optional[str]
2321
) -> Tuple[int, int]:
@@ -34,18 +32,15 @@ def estimate_entity_count_and_size(
3432
count += 1
3533
return count, total_size
3634

37-
3835
def analyze_kinds(config: AppConfig) -> List[Dict]:
3936
client = build_client(config)
4037

41-
# Determine namespaces: explicit list, or all
42-
namespaces = config.namespaces if config.namespaces else list_namespaces(client)
38+
# Thanks to config.py normalisation, [] is the only “all” case
39+
namespaces = config.namespaces or list_namespaces(client)
4340

4441
results: List[Dict] = []
4542
for ns in namespaces:
46-
# Determine kinds: explicit list, or all in namespace
47-
kinds = config.kinds if config.kinds else list_kinds(client, ns)
48-
43+
kinds = config.kinds or list_kinds(client, ns)
4944
logger.info("Analyzing namespace=%s, %d kinds", ns or "(default)", len(kinds))
5045
for kind in kinds:
5146
count, total_bytes = estimate_entity_count_and_size(client, kind, ns)
@@ -60,10 +55,9 @@ def analyze_kinds(config: AppConfig) -> List[Dict]:
6055
)
6156
return results
6257

63-
6458
def print_summary_table(rows: List[Dict]) -> None:
6559
# Plain stdout table for wide compatibility
6660
print("namespace,kind,count,size,bytes")
6761
for r in rows:
6862
ns = r.get("namespace") or ""
69-
print(f"{ns},{r['kind']},{r['count']},{r['size']},{r['bytes']}")
63+
print(f"{ns},{r['kind']},{r['count']},{r['size']},{r['bytes']}")

gcd_tools/config.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ def load_config(path: Optional[str] = None, overrides: Optional[Dict] = None) ->
6565
config.namespaces = _as_list(merged.get("namespaces"))
6666
config.kinds = _as_list(merged.get("kinds"))
6767

68+
# 🛠 Normalise: treat [""] as empty
69+
if config.namespaces == [""] or config.namespaces is None:
70+
config.namespaces = []
71+
if config.kinds == [""] or config.kinds is None:
72+
config.kinds = []
73+
6874
# Optional defaults used by some commands
6975
config.kind = merged.get("kind")
7076
config.namespace = merged.get("namespace")
@@ -81,6 +87,7 @@ def load_config(path: Optional[str] = None, overrides: Optional[Dict] = None) ->
8187
return config
8288

8389

90+
8491
def _configure_logging(level: str) -> None:
8592
level_value = getattr(logging, level.upper(), logging.INFO)
8693
logging.basicConfig(level=level_value, format="%(asctime)s | %(levelname)s | %(message)s")
@@ -105,14 +112,22 @@ def build_client(config: AppConfig) -> datastore.Client:
105112

106113

107114
def list_namespaces(client: datastore.Client) -> List[str]:
108-
# Include default namespace as "" first
115+
"""
116+
Return all namespaces in the datastore, including the default ("").
117+
Always queries __namespace__ in the root context so it works in emulator/GCP.
118+
"""
119+
# Include default namespace "" first
109120
namespaces: List[str] = [""]
110-
query = client.query(kind="__namespace__")
121+
122+
# Force namespace=None to query the metadata root
123+
query = client.query(kind="__namespace__", namespace=None)
111124
query.keys_only()
125+
112126
for entity in query.fetch():
113127
name = entity.key.name or ""
114128
if name != "":
115129
namespaces.append(name)
130+
116131
return namespaces
117132

118133

0 commit comments

Comments
 (0)