Skip to content

Commit 8d6230e

Browse files
committed
fix: resolve 27 mypy type errors, fix 3 test failures
Mypy fixes (27 errors → 0): - Replace datetime.UTC with datetime.timezone.utc (7 occurrences) - Add explicit type annotations to avoid Returning Any errors (5 files) - Rename output param to dest in template.py to avoid module shadowing - Remove stale type: ignore comments (3 occurrences) - Add ConfigBackend protocol annotation for polymorphic backend vars - Add explicit Path/str casts for return values Test fixes (3 failures → 0): - Move --json flag before subcommand in test_cli_dynamo.py (global flag) - Add scan_http_port param to fake_probe in test_web_server.py - Emit JSON error via emit_json() instead of suppressed output.error() Result: 334/334 tests pass, mypy 0 errors across 32 files
1 parent 24d1bc1 commit 8d6230e

File tree

12 files changed

+135
-100
lines changed

12 files changed

+135
-100
lines changed

tests/test_cli_dynamo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def test_status_success(self, aws_env):
195195
def test_status_json(self, aws_env):
196196
with mock_aws():
197197
_provision_resources()
198-
result = runner.invoke(app, ["dynamo", "status", "--json"])
198+
result = runner.invoke(app, ["--json", "dynamo", "status"])
199199
assert result.exit_code == 0, result.output
200200
data = json.loads(result.output)
201201
assert data["table_name"] == "test-zebra-config"
@@ -410,7 +410,7 @@ def test_status_json_no_prompt_fails(self, aws_env, monkeypatch):
410410
monkeypatch.setattr("zebra_day.cli.dynamo._is_interactive", lambda: True)
411411
with mock_aws():
412412
_provision_resources()
413-
result = runner.invoke(app, ["dynamo", "status", "--json"])
413+
result = runner.invoke(app, ["--json", "dynamo", "status"])
414414
assert result.exit_code == 1
415415
assert "ZEBRA_DAY_S3_BACKUP_BUCKET" in result.output
416416

tests/test_web_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ def fake_probe(
585585
lab: str,
586586
cancel_event=None,
587587
progress_callback=None,
588+
scan_http_port=None,
588589
):
589590
total = 255
590591
ip = f"{ip_stub}.1"

zebra_day/backends/dynamo.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,7 @@ def __init__(
5858
self.s3_prefix = s3_prefix.rstrip("/") + "/" if s3_prefix else "zebra-day/"
5959
self.client_id = client_id or f"{platform.node()}.{getpass.getuser()}"
6060
self.cost_center = cost_center or os.environ.get("LSMC_COST_CENTER", "global")
61-
self.project = project or os.environ.get(
62-
"LSMC_PROJECT", f"zebra-day+{self.region}"
63-
)
61+
self.project = project or os.environ.get("LSMC_PROJECT", f"zebra-day+{self.region}")
6462

6563
session_kwargs: dict[str, Any] = {"region_name": self.region}
6664
if profile:
@@ -80,7 +78,7 @@ def __init__(
8078
# ------------------------------------------------------------------
8179

8280
@classmethod
83-
def from_env(cls, *, allow_missing_bucket: bool = False) -> "DynamoBackend":
81+
def from_env(cls, *, allow_missing_bucket: bool = False) -> DynamoBackend:
8482
"""Create a DynamoBackend from environment variables.
8583
8684
Args:
@@ -182,7 +180,11 @@ def check_aws_permissions(self) -> dict[str, Any]:
182180
code = exc.response["Error"]["Code"]
183181
if code == "ResourceNotFoundException":
184182
result["checks"].append(
185-
{"action": "dynamodb:DescribeTable", "ok": True, "detail": "table not found (will be created)"}
183+
{
184+
"action": "dynamodb:DescribeTable",
185+
"ok": True,
186+
"detail": "table not found (will be created)",
187+
}
186188
)
187189
else:
188190
result["checks"].append(
@@ -201,11 +203,19 @@ def check_aws_permissions(self) -> dict[str, Any]:
201203
code = exc.response["Error"]["Code"]
202204
if code in ("404", "NoSuchBucket"):
203205
result["checks"].append(
204-
{"action": "s3:HeadBucket", "ok": True, "detail": "bucket not found (will be created)"}
206+
{
207+
"action": "s3:HeadBucket",
208+
"ok": True,
209+
"detail": "bucket not found (will be created)",
210+
}
205211
)
206212
elif code == "403":
207213
result["checks"].append(
208-
{"action": "s3:HeadBucket", "ok": False, "detail": "access denied — check S3 permissions"}
214+
{
215+
"action": "s3:HeadBucket",
216+
"ok": False,
217+
"detail": "access denied — check S3 permissions",
218+
}
209219
)
210220
result["all_ok"] = False
211221
else:
@@ -288,7 +298,7 @@ def write_meta(self) -> None:
288298
"""Write the META#table_info item (used by `zday dynamo init`)."""
289299
from zebra_day import __version__
290300

291-
now = datetime.datetime.now(datetime.UTC).isoformat()
301+
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
292302
self._table.put_item(
293303
Item={
294304
"PK": "META",
@@ -324,12 +334,13 @@ def load_config(self) -> dict:
324334

325335
config_data = item.get("config_data", "{}")
326336
if isinstance(config_data, str):
327-
return json.loads(config_data)
337+
result: dict[str, Any] = json.loads(config_data)
338+
return result
328339
return dict(config_data)
329340

330341
def save_config(self, config: dict) -> None:
331342
"""Save the printer configuration to DynamoDB with optimistic locking."""
332-
now = datetime.datetime.now(datetime.UTC).isoformat()
343+
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
333344
config_json = json.dumps(config, default=str)
334345

335346
# Get current version
@@ -398,7 +409,7 @@ def list_templates(self) -> list[str]:
398409
def save_template(self, name: str, zpl_content: str) -> None:
399410
"""Save or overwrite a template in DynamoDB with optimistic locking."""
400411
stem = self._normalize_stem(name)
401-
now = datetime.datetime.now(datetime.UTC).isoformat()
412+
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
402413
current_version = self._get_item_version("TEMPLATE", stem)
403414
content = str(zpl_content)
404415

@@ -451,7 +462,6 @@ def template_exists(self, name: str) -> bool:
451462
)
452463
return "Item" in resp
453464

454-
455465
# ------------------------------------------------------------------
456466
# S3 Backup
457467
# ------------------------------------------------------------------
@@ -460,7 +470,7 @@ def backup_to_s3(self, *, triggered_by: str = "manual", force: bool = False) ->
460470
"""Write a full snapshot to S3. Returns the S3 key prefix of the backup."""
461471
from zebra_day import __version__
462472

463-
now = datetime.datetime.now(datetime.UTC)
473+
now = datetime.datetime.now(datetime.timezone.utc)
464474
ts = now.strftime("%Y-%m-%dT%H-%M-%SZ")
465475
prefix = f"{self.s3_prefix}backups/{ts}/"
466476

@@ -551,7 +561,7 @@ def restore_from_s3(self, s3_prefix: str) -> None:
551561
config = json.loads(resp["Body"].read().decode())
552562

553563
# Write config to DDB (bypass optimistic locking for restore)
554-
now = datetime.datetime.now(datetime.UTC).isoformat()
564+
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
555565
self._table.put_item(
556566
Item={
557567
"PK": "CONFIG",
@@ -680,4 +690,4 @@ def _normalize_stem(name: str) -> str:
680690
raw = raw[:-4]
681691
if not raw:
682692
raise ValueError("Template name cannot be empty")
683-
return raw
693+
return raw

zebra_day/backends/local.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def save_template(self, name: str, zpl_content: str) -> None:
130130

131131
# Backup existing
132132
if target.exists():
133-
ts = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d_%H%M%S.%fZ")
133+
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d_%H%M%S.%fZ")
134134
backup = target.parent / f"{stem}.bak.{ts}.zpl"
135135
shutil.copy2(target, backup)
136136

@@ -156,9 +156,7 @@ def template_exists(self, name: str) -> bool:
156156
# Path resolution (local-only helpers used by zpl() for compat)
157157
# ------------------------------------------------------------------
158158

159-
def resolve_template_path(
160-
self, template: str, *, include_legacy_drafts: bool = False
161-
) -> Path:
159+
def resolve_template_path(self, template: str, *, include_legacy_drafts: bool = False) -> Path:
162160
"""Resolve a template name to an on-disk .zpl path.
163161
164162
Resolution order:
@@ -264,4 +262,3 @@ def _migrate_json_to_yaml(self, json_path: Path, yaml_path: Path) -> None:
264262
backup_dir = xdg.get_config_backups_dir()
265263
backup_name = f"{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_migrated_from.json"
266264
shutil.copy2(json_path, backup_dir / backup_name)
267-

zebra_day/cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def _root_callback(
144144
"""Initialize RuntimeContext for the current invocation."""
145145
_reset()
146146
debug = os.environ.get("CLI_CORE_YO_DEBUG") == "1"
147-
xdg_paths = app._cli_core_yo_xdg_paths # type: ignore[attr-defined]
147+
xdg_paths = app._cli_core_yo_xdg_paths
148148
initialize(spec, xdg_paths, json_mode=json_flag, debug=debug)
149149

150150

zebra_day/cli/dynamo.py

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def _prompt_s3_bucket() -> str:
3737
3838
Raises ``typer.Exit(1)`` if the user provides an empty value.
3939
"""
40-
bucket = typer.prompt("S3 bucket name for backups").strip()
40+
bucket: str = str(typer.prompt("S3 bucket name for backups")).strip()
4141
if not bucket:
4242
output.error("No S3 bucket name provided.")
4343
raise typer.Exit(1)
@@ -58,9 +58,7 @@ def _ensure_s3_bucket(backend, *, create_if_missing: bool = False) -> None:
5858
backend._s3.head_bucket(Bucket=backend.s3_bucket)
5959
except Exception:
6060
if create_if_missing:
61-
output.action(
62-
f"Bucket '{backend.s3_bucket}' not found — creating..."
63-
)
61+
output.action(f"Bucket '{backend.s3_bucket}' not found — creating...")
6462
backend.create_s3_bucket()
6563
output.success(
6664
f"S3 bucket '{backend.s3_bucket}' created "
@@ -92,7 +90,15 @@ def _get_backend_from_env(
9290
backend = DynamoBackend.from_env(allow_missing_bucket=True)
9391

9492
if not backend.s3_bucket:
95-
if not get_context().json_mode and _is_interactive():
93+
if get_context().json_mode:
94+
output.emit_json(
95+
{
96+
"error": "ZEBRA_DAY_S3_BACKUP_BUCKET is required when using "
97+
"DynamoDB backend. Set it to the S3 bucket name for config backups."
98+
}
99+
)
100+
raise typer.Exit(1)
101+
elif _is_interactive():
96102
output.warning("ZEBRA_DAY_S3_BACKUP_BUCKET is not set.")
97103
bucket = _prompt_s3_bucket()
98104
backend.s3_bucket = bucket
@@ -165,9 +171,7 @@ def _get_backend(
165171

166172
# S3 bucket resolution: flag > config file > env var > interactive prompt
167173
resolved_bucket = (
168-
s3_bucket
169-
or file_cfg.get("s3_bucket")
170-
or os.environ.get("ZEBRA_DAY_S3_BACKUP_BUCKET")
174+
s3_bucket or file_cfg.get("s3_bucket") or os.environ.get("ZEBRA_DAY_S3_BACKUP_BUCKET")
171175
)
172176
if not resolved_bucket:
173177
if _is_interactive():
@@ -208,6 +212,7 @@ def _get_backend(
208212
# init
209213
# -----------------------------------------------------------------
210214

215+
211216
@dynamo_app.command("init")
212217
def init_cmd(
213218
table_name: str = typer.Option(
@@ -216,18 +221,14 @@ def init_cmd(
216221
region: str = typer.Option(
217222
None, "--region", "-r", help="AWS region [default: env or us-east-1]"
218223
),
219-
s3_bucket: str = typer.Option(
220-
None, "--s3-bucket", "-b", help="S3 bucket for backups"
221-
),
222-
s3_prefix: str = typer.Option(
223-
"zebra-day/", "--s3-prefix", help="S3 key prefix"
224-
),
224+
s3_bucket: str = typer.Option(None, "--s3-bucket", "-b", help="S3 bucket for backups"),
225+
s3_prefix: str = typer.Option("zebra-day/", "--s3-prefix", help="S3 key prefix"),
225226
s3_config_file: str = typer.Option(
226-
None, "--s3-config-file", help="JSON file with S3 bucket config (keys: s3_bucket, s3_prefix, region)"
227-
),
228-
profile: str = typer.Option(
229-
None, "--profile", "-p", help="AWS profile name"
227+
None,
228+
"--s3-config-file",
229+
help="JSON file with S3 bucket config (keys: s3_bucket, s3_prefix, region)",
230230
),
231+
profile: str = typer.Option(None, "--profile", "-p", help="AWS profile name"),
231232
cost_center: str = typer.Option(
232233
None, "--cost-center", help="lsmc-cost-center tag [default: env or 'global']"
233234
),
@@ -274,8 +275,7 @@ def init_cmd(
274275

275276
if not perm_result["all_ok"]:
276277
output.error(
277-
"Permission checks failed. "
278-
"Fix the issues above or use --skip-checks to bypass."
278+
"Permission checks failed. Fix the issues above or use --skip-checks to bypass."
279279
)
280280
raise typer.Exit(1)
281281
output.success("All permission checks passed")
@@ -286,7 +286,10 @@ def init_cmd(
286286
backend.create_table()
287287
output.success(f"Table '{backend.table_name}' created and active")
288288
except Exception as exc:
289-
if "ResourceInUseException" in str(type(exc).__name__) or "already exists" in str(exc).lower():
289+
if (
290+
"ResourceInUseException" in str(type(exc).__name__)
291+
or "already exists" in str(exc).lower()
292+
):
290293
output.warning(f"Table '{backend.table_name}' already exists")
291294
else:
292295
output.error(f"Failed to create table: {exc}")
@@ -308,7 +311,7 @@ def init_cmd(
308311

309312
# Print env var instructions
310313
output.heading("Set these environment variables")
311-
output.detail(f"export ZEBRA_DAY_CONFIG_BACKEND=dynamodb")
314+
output.detail("export ZEBRA_DAY_CONFIG_BACKEND=dynamodb")
312315
output.detail(f"export ZEBRA_DAY_DYNAMO_TABLE={backend.table_name}")
313316
output.detail(f"export ZEBRA_DAY_DYNAMO_REGION={backend.region}")
314317
output.detail(f"export ZEBRA_DAY_S3_BACKUP_BUCKET={backend.s3_bucket}")
@@ -319,6 +322,7 @@ def init_cmd(
319322
# status
320323
# -----------------------------------------------------------------
321324

325+
322326
@dynamo_app.command("status")
323327
def status_cmd(
324328
create_s3_if_missing: bool = typer.Option(
@@ -337,7 +341,9 @@ def status_cmd(
337341
try:
338342
status = backend.get_status()
339343
except Exception as exc:
340-
if "not found" in str(exc).lower() or "ResourceNotFoundException" in str(type(exc).__name__):
344+
if "not found" in str(exc).lower() or "ResourceNotFoundException" in str(
345+
type(exc).__name__
346+
):
341347
output.error("Table not found. Run 'zday dynamo init' first.")
342348
raise typer.Exit(1)
343349
raise
@@ -356,7 +362,12 @@ def status_cmd(
356362
tbl.add_column("Value")
357363
tbl.add_row("Table", status["table_name"])
358364
tbl.add_row("Region", status["region"])
359-
tbl.add_row("Status", f"[green]{status['table_status']}[/green]" if status["table_status"] == "ACTIVE" else status["table_status"])
365+
tbl.add_row(
366+
"Status",
367+
f"[green]{status['table_status']}[/green]"
368+
if status["table_status"] == "ACTIVE"
369+
else status["table_status"],
370+
)
360371
tbl.add_row("Items", str(status["item_count"]))
361372
tbl.add_row("Templates", str(template_count))
362373
tbl.add_row("Config Version", str(status["config_version"]))
@@ -373,6 +384,7 @@ def status_cmd(
373384
# bootstrap
374385
# -----------------------------------------------------------------
375386

387+
376388
@dynamo_app.command("bootstrap")
377389
def bootstrap_cmd(
378390
config_file: str = typer.Option(
@@ -447,22 +459,22 @@ def bootstrap_cmd(
447459
# Force backup
448460
prefix = backend.backup_to_s3(triggered_by="bootstrap", force=True)
449461
output.success(f"Backup written to s3://{backend.s3_bucket}/{prefix}")
450-
output.success(f"Bootstrap complete: {config_items_written} config + {templates_written} templates")
451-
462+
output.success(
463+
f"Bootstrap complete: {config_items_written} config + {templates_written} templates"
464+
)
452465

453466

454467
# -----------------------------------------------------------------
455468
# export
456469
# -----------------------------------------------------------------
457470

471+
458472
@dynamo_app.command("export")
459473
def export_cmd(
460474
output_dir: str = typer.Option(
461475
"./zebra-day-export", "--output-dir", "-o", help="Target directory"
462476
),
463-
fmt: str = typer.Option(
464-
"json", "--format", "-f", help="Config format: json or yaml"
465-
),
477+
fmt: str = typer.Option("json", "--format", "-f", help="Config format: json or yaml"),
466478
):
467479
"""Pull DynamoDB config and templates to local files."""
468480
try:
@@ -506,6 +518,7 @@ def export_cmd(
506518
# backup
507519
# -----------------------------------------------------------------
508520

521+
509522
@dynamo_app.command("backup")
510523
def backup_cmd(
511524
create_s3_if_missing: bool = typer.Option(
@@ -533,14 +546,13 @@ def backup_cmd(
533546
# restore
534547
# -----------------------------------------------------------------
535548

549+
536550
@dynamo_app.command("restore")
537551
def restore_cmd(
538552
s3_key: str = typer.Option(
539553
None, "--s3-key", "-k", help="S3 key prefix of the backup to restore"
540554
),
541-
list_backups: bool = typer.Option(
542-
False, "--list", "-l", help="List available backups"
543-
),
555+
list_backups: bool = typer.Option(False, "--list", "-l", help="List available backups"),
544556
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
545557
):
546558
"""Restore DynamoDB from an S3 backup."""
@@ -597,6 +609,7 @@ def restore_cmd(
597609
# destroy
598610
# -----------------------------------------------------------------
599611

612+
600613
@dynamo_app.command("destroy")
601614
def destroy_cmd(
602615
yes: bool = typer.Option(False, "--yes", "-y", help="Required safety gate"),

0 commit comments

Comments
 (0)