Skip to content

Commit 8ca20fe

Browse files
Move to command folder
1 parent f7635b5 commit 8ca20fe

File tree

9 files changed

+63
-47
lines changed

9 files changed

+63
-47
lines changed

cli.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
import typer
77

8-
from gcd_tools.config import AppConfig, load_config
9-
from gcd_tools.analyze_kinds import analyze_kinds, print_summary_table
10-
from gcd_tools.analyze_entity_fields import analyze_field_contributions, print_field_summary
11-
from gcd_tools.cleanup_expired import cleanup_expired
8+
from commands.config import AppConfig, load_config
9+
from commands.analyze_kinds import analyze_kinds, print_summary_table
10+
from commands.analyze_entity_fields import analyze_field_contributions, print_field_summary
11+
from commands.cleanup_expired import cleanup_expired
1212

1313
app = typer.Typer(help="Utilities for analyzing and managing local Datastore/Firestore (Datastore mode)")
1414

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
from . import config as config
66

77
__all__ = [
8-
"AppConfig",
9-
"load_config",
10-
"build_client",
11-
"list_namespaces",
12-
"list_kinds",
13-
"format_size",
14-
"analyze_kinds",
15-
"analyze_entity_fields",
16-
"cleanup_expired",
17-
"config",
18-
]
8+
"AppConfig",
9+
"load_config",
10+
"build_client",
11+
"list_namespaces",
12+
"list_kinds",
13+
"format_size",
14+
"analyze_kinds",
15+
"analyze_entity_fields",
16+
"cleanup_expired",
17+
"config",
18+
]
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,14 @@ def analyze_field_contributions(
125125
) -> Dict:
126126
client = build_client(config)
127127

128-
# If no namespace provided, iterate across all namespaces
128+
# If no namespace provided, or config.namespaces is None/empty, iterate all namespaces
129129
if namespace is None:
130+
if hasattr(config, "namespaces") and (not config.namespaces):
131+
ns_list = list_namespaces(client)
132+
else:
133+
ns_list = [namespace] if namespace else list_namespaces(client)
130134
results: Dict[str, Dict] = {}
131-
for ns in list_namespaces(client):
135+
for ns in ns_list:
132136
results[ns or ""] = _analyze_single_namespace(
133137
client, kind=kind, namespace=ns, group_by_field=group_by_field, only_fields=only_fields
134138
)
@@ -161,4 +165,4 @@ def print_field_summary(result: Dict) -> None:
161165
)
162166
for field, stats in result["fields"].items():
163167
avg = stats["avg_per_entity"]
164-
print(f" {field:30} {stats['human']:>12} ({avg:.1f} bytes avg)")
168+
print(f" {field:30} {stats['human']:>12} ({avg:.1f} bytes avg)")
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,18 @@ def analyze_kinds(config: AppConfig, method: Optional[str] = None) -> List[Dict]
7070
# Decide method priority: parameter > config > default
7171
method = method or getattr(config, "method", None) or "stats"
7272

73-
# Thanks to config.py normalisation, [] is the only “all” case
74-
namespaces = config.namespaces or list_namespaces(client)
73+
# If namespaces is None or empty, iterate all available namespaces
74+
if not config.namespaces:
75+
namespaces = list_namespaces(client)
76+
else:
77+
namespaces = config.namespaces
7578

79+
print(f"Found namespaces: {namespaces}")
7680
from tqdm import tqdm
7781
results: List[Dict] = []
7882
for ns in namespaces:
7983
kinds = config.kinds or list_kinds(client, ns)
84+
print(f"Namespace '{ns}': found kinds: {kinds}")
8085
logger.info("Analyzing namespace=%s, %d kinds", ns or "(default)", len(kinds))
8186
for kind in tqdm(kinds, desc=f"Analyzing kinds in ns={ns or '(default)'}", unit="kind"):
8287
if method == "stats":
Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
from google.cloud import datastore
88

9-
from tqdm import tqdm
10-
119
from .config import (
1210
AppConfig,
1311
build_client,
@@ -33,8 +31,11 @@ def cleanup_expired(
3331
) -> Dict[str, int]:
3432
client = build_client(config)
3533

36-
# Determine namespaces: explicit list, or all
37-
namespaces = config.namespaces if config.namespaces else list_namespaces(client)
34+
# If namespaces is None or empty, iterate all available namespaces
35+
if not config.namespaces:
36+
namespaces = list_namespaces(client)
37+
else:
38+
namespaces = config.namespaces
3839

3940
totals: Dict[str, int] = {}
4041
now = datetime.now(timezone.utc)
@@ -47,6 +48,7 @@ def cleanup_expired(
4748
query = client.query(kind=kind, namespace=ns or None)
4849
to_delete: List[datastore.Key] = []
4950
entities = list(query.fetch())
51+
from tqdm import tqdm
5052
for entity in tqdm(entities, desc=f"Scanning {kind} in ns={ns or '(default)'}", unit="entity"):
5153
expire_at = entity.get(config.ttl_field)
5254
expired = expire_at is None if config.delete_missing_ttl else False
@@ -68,17 +70,17 @@ def cleanup_expired(
6870
)
6971
totals[f"{ns}:{kind}"] = len(to_delete)
7072
else:
71-
deleted = 0
72-
if to_delete:
73-
for batch in tqdm(list(chunked(to_delete, config.batch_size)), desc=f"Deleting {kind} in ns={ns or '(default)'}", unit="batch"):
74-
client.delete_multi(batch)
75-
deleted += len(batch)
76-
logger.info(
77-
"ns=%s kind=%s deleted %d expired entities",
78-
ns or "(default)",
79-
kind,
80-
deleted,
81-
)
82-
totals[f"{ns}:{kind}"] = deleted
73+
deleted = 0
74+
if to_delete:
75+
for batch in tqdm(list(chunked(to_delete, config.batch_size)), desc=f"Deleting {kind} in ns={ns or '(default)'}", unit="batch"):
76+
client.delete_multi(batch)
77+
deleted += len(batch)
78+
logger.info(
79+
"ns=%s kind=%s deleted %d expired entities",
80+
ns or "(default)",
81+
kind,
82+
deleted,
83+
)
84+
totals[f"{ns}:{kind}"] = deleted
8385

84-
return totals
86+
return totals
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import yaml
99
from google.cloud import datastore
1010

11-
1211
@dataclass
1312
class AppConfig:
1413
project_id: Optional[str] = None
@@ -87,7 +86,6 @@ def load_config(path: Optional[str] = None, overrides: Optional[Dict] = None) ->
8786
return config
8887

8988

90-
9189
def _configure_logging(level: str) -> None:
9290
level_value = getattr(logging, level.upper(), logging.INFO)
9391
logging.basicConfig(level=level_value, format="%(asctime)s | %(levelname)s | %(message)s")
@@ -148,4 +146,4 @@ def format_size(bytes_size: int) -> str:
148146
if size < 1024:
149147
return f"{size:.2f} {unit}"
150148
size /= 1024
151-
return f"{size:.2f} PB"
149+
return f"{size:.2f} PB"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ local-storage-utils = "cli:app"
3838

3939
[tool.setuptools.packages.find]
4040
where = ["."]
41-
include = ["gcd_tools*"]
41+
include = ["commands*"]
4242

4343
[tool.black]
4444
line-length = 100

tests/test_commands.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
21
import sys
32
import os
43
import pytest
54
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
6-
from gcd_tools import analyze_kinds, analyze_entity_fields, cleanup_expired, config
7-
from gcd_tools.config import AppConfig
5+
from commands import analyze_kinds, analyze_entity_fields, cleanup_expired, config
6+
from commands.config import AppConfig, build_client, list_namespaces
87

98
# Dummy config for testing (adjust as needed for emulator)
109
def make_dummy_config():
@@ -43,3 +42,11 @@ def test_cleanup_expired_runs():
4342
assert isinstance(result, dict)
4443
except Exception as e:
4544
pytest.skip(f"cleanup_expired requires emulator: {e}")
45+
46+
def test_list_namespaces_returns_default_and_any_custom():
47+
cfg = AppConfig(project_id="dummy-project", emulator_host="localhost:8010")
48+
client = build_client(cfg)
49+
namespaces = list_namespaces(client)
50+
assert "" in namespaces # default namespace always present
51+
# This test will pass if at least the default namespace is present
52+
# Add more asserts if you know your emulator has more namespaces

tests/test_import.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
44

55
def test_imports():
6-
import gcd_tools
7-
from gcd_tools import analyze_kinds, analyze_entity_fields, cleanup_expired, config
6+
import commands
7+
from commands import analyze_kinds, analyze_entity_fields, cleanup_expired, config
88

9-
assert gcd_tools is not None
9+
assert commands is not None
1010
assert hasattr(config, "AppConfig")

0 commit comments

Comments
 (0)