pushframe 5.1.26__tar.gz → 5.1.28__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pushframe-5.1.26 → pushframe-5.1.28}/CHANGELOG.md +53 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/CLAUDE.md +3 -3
- {pushframe-5.1.26 → pushframe-5.1.28}/PKG-INFO +1 -1
- {pushframe-5.1.26 → pushframe-5.1.28}/docs/CLI.md +10 -1
- {pushframe-5.1.26 → pushframe-5.1.28}/docs/ERRORS.md +5 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/__init__.py +1 -1
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/assetApi.py +2 -2
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/frameApi.py +1 -1
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/cli.py +155 -10
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/schedule.py +5 -1
- pushframe-5.1.28/pushframe/utils/dt.py +27 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pyproject.toml +1 -1
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_asset_partial.py +3 -3
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_schedule.py +285 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_version.py +1 -1
- {pushframe-5.1.26 → pushframe-5.1.28}/uv.lock +1 -1
- pushframe-5.1.26/pushframe/utils/dt.py +0 -15
- {pushframe-5.1.26 → pushframe-5.1.28}/.README-orig.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.env.sample +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.freebuff/project-id +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.github/workflows/release.yml +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.github/workflows/tests.yml +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.gitignore +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.gsd/dispatch-isolation-sentinel.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/MILESTONES.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/PROJECT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/REQUIREMENTS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/RETROSPECTIVE.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/ROADMAP.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/STATE.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/WINDOWS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/ARCHITECTURE.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/CONCERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/CONVENTIONS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/INTEGRATIONS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/STACK.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/STRUCTURE.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/TESTING.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/config.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/gsync-apply-budget-none-crash.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/knowledge-base.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/login-475-null-creds.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/resolved/cli-status-verbose-logging.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/resolved/inspect-asset-null-fields.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/resolved/select-asset-401-unauthorized.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/status-crash-401-trip.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-ROADMAP.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-LIVE-EVIDENCE.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-ROADMAP.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/.gitkeep +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-SECURITY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-UAT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-ROADMAP.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-SECURITY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-UAT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-SECURITY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW-FIX.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-SECURITY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-LIVE-FINDINGS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW-FIX.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/.gitkeep +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW-FIX.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/.gitkeep +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-LIVE-FINDINGS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-SECURITY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-UAT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/COVERAGE.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/deferred-items.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/README.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-ROADMAP.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-LIVE-FINDINGS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-PATTERNS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-REVIEW.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-ROADMAP.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DECISION-RECORD.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-LIVE-FINDINGS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/COVERAGE.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-DISCUSS-CHECKPOINT.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/COVERAGE.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-UAT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-ROADMAP.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/20-pushframe-rename-migration-repo-switch/20-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/OPERATOR-STEPS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-PLAN.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-CONTEXT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-RESEARCH.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-VERIFICATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/ALBUM-ACCESS-V4-ADDENDUM.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/ALBUM-ACCESS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/ARCHITECTURE.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/BROWSER-AUTOMATION.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/FEATURES.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/PITFALLS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/STACK.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/SUMMARY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/state.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.planning/todos/completed/2026-07-06-promote-debug-flag-to-a-global-cli-convention.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/.python-version +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/LICENSE +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/README.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/SECURITY.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/VERIFICATION-REPORT.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/docs/DEVELOPING.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/docs/INTERNALS.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/docs/agents/domain.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/docs/agents/issue-tracker.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/docs/agents/triage-labels.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/docs/superpowers/specs/2026-07-09-write-rate-limiter-design.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/hatch_build.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/main.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/packaging/control-template +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/packaging/lintian-overrides +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/packaging/postinst +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/packaging/pushframe.copyright +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/probes/.gitignore +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/probes/README.md +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/probes/browser_bootstrap.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/probes/common.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/probes/cookie_vault.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/probes/fidelity_check.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/probes/rpc_capture.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/probes/shared_link_probe.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/__init__.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/accountApi.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/activityApi.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/baseApi.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/notificationApi.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/peopleApi.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/playlistApi.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aura.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aws/__init__.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aws/awsclient.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aws/s3client.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aws/sqsclient.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/cache.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/client.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/config_store.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/doctor.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/exif.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/export.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/__init__.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/bootstrap.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/cache.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/client.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/enumerate.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/manifest.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/parsers.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/redaction.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/vault.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/gsync.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/migration.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/__init__.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/activity.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/asset.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/frame.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/meta.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/person.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/user.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/pairs.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/preflight.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/ratelimit.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/reconcile.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/report.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/session.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/sync.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/utils/io.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/utils/settings.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/scripts/build-deb.sh +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/scripts/make-apt-key.sh +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/scripts/publish-apt-repo.sh +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/scripts/release.sh +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-apt-journey-container.sh +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-config-journey-container.sh +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-deb-container.sh +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-install-journey-container.sh +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-multipair-journey-container.sh +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/conftest.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/asset_unprocessed.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/assets_page1.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/assets_page2.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/assets_placeholders.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/error_envelope.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/frame_detail.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/frames.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/google_rpc_snackc_page.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/google_share_page_sample.html +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/login.json +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/probe_af_initdata_sample.html +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/offline.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_asset_unprocessed.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_aura_logger.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_batch_update_contract.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_budget_identity.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_build_hook.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_apply.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_google_album.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_google_link.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_inspect.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_push_budget_geo.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_reconcile.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_status.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_status_google.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_sync.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_client_rate_limit.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_config_wizard.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_docs_hint.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_doctor.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_execute_plan.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_execute_plan_budget_geo.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_fixtures_validity.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_cache.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_client.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_enumerate.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_manifest.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_parsers.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_vault.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_gsync_all.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_gsync_engine.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_gsync_execute.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_imports.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_logout.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_migration.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_network_guard.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_offline_read_path.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_pairs.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_preflight_sweep.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_preflight_vault.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_prep_upload_formats.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_probe_browser.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_probe_shared_link.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_ratelimit.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_read_path.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_reconcile.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_report.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_retry_401.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_session.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_settings_budget.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_settings_resolution.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_sync_engine.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_token_refresh_extension.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_write_endpoints_failloud.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_write_formats_live.py +0 -0
- {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_write_throttling.py +0 -0
|
@@ -12,6 +12,59 @@ every channel.
|
|
|
12
12
|
`pushframe` is an unofficial community CLI for Aura Frames digital photo
|
|
13
13
|
frames; it is not affiliated with Aura Frames Inc.
|
|
14
14
|
|
|
15
|
+
## [5.1.28] - 2026-10-04
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Scheduled `push` jobs (cron or hand-written units feeding a "buffet"
|
|
20
|
+
directory) take `--scheduled`/`--report-tag` and write the same run
|
|
21
|
+
bookkeeping as scheduled `google-sync`/`sync`: the `=== run … ===`
|
|
22
|
+
header with the job tag, the `=== run end rc=<rc> elapsed=<s>s ===`
|
|
23
|
+
footer, then the 5 MiB rotation. `push` stays purely additive —
|
|
24
|
+
bookkeeping only, no removal guard involved.
|
|
25
|
+
|
|
26
|
+
## [5.1.27] - 2026-10-03
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `schedule add --sync-dir` units never ran: their ExecStart passed
|
|
31
|
+
`--scheduled`, which the `sync` parser never accepted — every tick died
|
|
32
|
+
with `unrecognized arguments: --scheduled` before any sync happened. The
|
|
33
|
+
parser now takes `--scheduled` and `--report-tag`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `status` flags any installed scheduled unit whose ExecStart the current
|
|
38
|
+
binary's argparse refuses (`⚠ would FAIL to start: …` with the exact
|
|
39
|
+
reason) — hand-edited or version-drifted units otherwise die on every
|
|
40
|
+
tick with nothing but journalctl to explain why. The same contract is
|
|
41
|
+
enforced offline by a test over every ExecStart `schedule_add` can
|
|
42
|
+
generate, and on the host by a deliberate `pytest -m live` check over
|
|
43
|
+
the real installed units.
|
|
44
|
+
- Scheduled `sync` jobs (`--sync-dir`) write the same run bookkeeping as
|
|
45
|
+
scheduled `google-sync`: the `=== run … ===` header (with the job tag),
|
|
46
|
+
the `=== run end rc=<rc> elapsed=<s>s ===` footer, and the 5 MiB log
|
|
47
|
+
rotation — `<job>.log` alone answers which night synced what. Schedule
|
|
48
|
+
units now pass `--report-tag "<job>"`; `--report` (email) remains
|
|
49
|
+
google-sync-only.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- Replaced the deprecated `datetime.utcnow()` in `get_utc_now()` with the
|
|
54
|
+
deprecation-free naive-UTC equivalent
|
|
55
|
+
(`datetime.now(timezone.utc).replace(tzinfo=None)`) — same value, same
|
|
56
|
+
naive contract every caller relies on (`parse_aura_dt` diffs,
|
|
57
|
+
`WriteBudget` arithmetic and its persisted `updated_at`).
|
|
58
|
+
- Replaced Pydantic's deprecated `.dict()` with `.model_dump()` in the
|
|
59
|
+
API request-payload builders (`AssetApi.batch_update`,
|
|
60
|
+
`AssetApi.crop_asset`, `FrameApi.update_frame`) — Python 3.13-clean,
|
|
61
|
+
ready for Pydantic V3.
|
|
62
|
+
- Job log rotation keeps a bounded history instead of a single generation:
|
|
63
|
+
past 5 MiB the log slides through `<job>.log.1` … `<job>.log.28`
|
|
64
|
+
(default; `PUSHFRAME_JOB_LOG_KEEP` tunes it), the oldest generation
|
|
65
|
+
falling off — several weeks of consultable history, worst case
|
|
66
|
+
`keep × 5 MiB` per job.
|
|
67
|
+
|
|
15
68
|
## [5.1.26] - 2026-10-03
|
|
16
69
|
|
|
17
70
|
### Added
|
|
@@ -137,7 +137,7 @@ Python toolchain, so we know exactly what survives before building anything new.
|
|
|
137
137
|
- Partial model pattern via custom `AllOptional` metaclass (`meta.py`): `class FramePartial(Frame, metaclass=AllOptional)`
|
|
138
138
|
- Enums used for known string-valued discriminators: `ActivityType`, `ReactionType`, `Feature`
|
|
139
139
|
- Pydantic `@validator` used for cross-field validation: `AssetPartialId.check_id_or_local_id`
|
|
140
|
-
- `.
|
|
140
|
+
- `.model_dump(include={...})` used to build API request payloads from model instances
|
|
141
141
|
## Configuration
|
|
142
142
|
- Credentials passed via env vars: `AURA_EMAIL`, `AURA_PASSWORD`
|
|
143
143
|
- AWS pool IDs hardcoded as module constants (flagged as TODO to move to config)
|
|
@@ -172,7 +172,7 @@ Python toolchain, so we know exactly what survives before building anything new.
|
|
|
172
172
|
## Pattern Overview
|
|
173
173
|
- `Aura` is the single public entry point; callers never instantiate API classes directly
|
|
174
174
|
- Each `*Api` class is a thin wrapper over one REST resource domain with zero business logic of its own
|
|
175
|
-
- Pydantic models serve as both validated DTOs and serialization targets (`.
|
|
175
|
+
- Pydantic models serve as both validated DTOs and serialization targets (`.model_dump()` for request payloads, `**json_response` for hydration)
|
|
176
176
|
- AWS auth uses Cognito anonymous identity pools — no long-lived AWS credentials needed
|
|
177
177
|
- All HTTP is synchronous (`httpx.Client`); the codebase has acknowledged debt to migrate to async
|
|
178
178
|
## Layers
|
|
@@ -231,7 +231,7 @@ Python toolchain, so we know exactly what survives before building anything new.
|
|
|
231
231
|
- Pattern: Constructor injection of `Client`; no interface/protocol defined
|
|
232
232
|
- Purpose: Validate, hydrate, and serialise API response JSON
|
|
233
233
|
- Examples: `auraframes/models/frame.py`, `auraframes/models/asset.py`
|
|
234
|
-
- Pattern: `Model(**json_response.get('key'))` for hydration; `.
|
|
234
|
+
- Pattern: `Model(**json_response.get('key'))` for hydration; `.model_dump(include={...})` for request payloads
|
|
235
235
|
- Purpose: Generates a "partial" variant of any model (all fields become `Optional`) for PATCH-style updates
|
|
236
236
|
- Examples: `auraframes/models/meta.py` — used by `FramePartial` in `auraframes/models/frame.py:93`
|
|
237
237
|
- Pattern: `class FramePartial(Frame, metaclass=AllOptional): pass`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pushframe
|
|
3
|
-
Version: 5.1.
|
|
3
|
+
Version: 5.1.28
|
|
4
4
|
Summary: Unofficial community CLI for Aura Frames digital photo frames (sync a local folder or a Google Photos album; not affiliated with Aura Frames Inc.)
|
|
5
5
|
Project-URL: Homepage, https://github.com/coredmp95/pushframe
|
|
6
6
|
Project-URL: Repository, https://github.com/coredmp95/pushframe
|
|
@@ -633,7 +633,7 @@ The second run reports `To upload: 0` — the photo is re-shown, never uploaded
|
|
|
633
633
|
usage: pushframe push [-h] --frame FRAME [--apply] [--yes] [--limit LIMIT]
|
|
634
634
|
[--batch-size BATCH_SIZE] [--chunk-delay CHUNK_DELAY]
|
|
635
635
|
[--max-wait MAX_WAIT] [--no-wait] [--country COUNTRY]
|
|
636
|
-
[--ignore-budget]
|
|
636
|
+
[--ignore-budget] [--scheduled] [--report-tag REPORT_TAG]
|
|
637
637
|
dir
|
|
638
638
|
|
|
639
639
|
positional arguments:
|
|
@@ -652,12 +652,21 @@ options:
|
|
|
652
652
|
--no-wait Stop immediately instead of waiting when the write budget is exhausted
|
|
653
653
|
--country COUNTRY Override the expected account country for the geo pre-flight guard
|
|
654
654
|
--ignore-budget Escape hatch: bypass the write budget entirely for this run
|
|
655
|
+
--scheduled Timed-run bookkeeping: the job log opens with a timestamped
|
|
656
|
+
"=== run ... ===" header and closes with the rc/elapsed footer
|
|
657
|
+
--report-tag REPORT_TAG Job identity in the log header and the log-rotation target
|
|
655
658
|
```
|
|
656
659
|
|
|
657
660
|
`push` is **structurally additive**: the removal list is forced empty, so it can never hide,
|
|
658
661
|
remove, or re-show anything. It is the safe way to add photos from a supply directory without
|
|
659
662
|
the frame being diffed to match it.
|
|
660
663
|
|
|
664
|
+
Cron-fed buffet frames: run `push ... --apply --yes --scheduled --report-tag <job>` from
|
|
665
|
+
cron or a hand-written unit — the job log (`~/.local/state/pushframe/<job>.log` for units)
|
|
666
|
+
then opens with a timestamped `=== run … ===` header, closes with `=== run end rc=<rc>
|
|
667
|
+
elapsed=<s>s ===`, and rotates past 5 MiB (`PUSHFRAME_JOB_LOG_KEEP` tunes the depth), like
|
|
668
|
+
every scheduled verb.
|
|
669
|
+
|
|
661
670
|
```bash
|
|
662
671
|
pushframe push ./buffet --frame "Living Room"
|
|
663
672
|
```
|
|
@@ -104,6 +104,11 @@ chosen per job at install time with `schedule add … --report LEVEL`
|
|
|
104
104
|
| `schedule add: --FLAG needs a value` / `unknown option "…"` / `… given twice` / `unexpected "…"` | The `schedule add` tail is malformed — a dangling flag, a typo in a flag name (they are listed), a duplicated flag, or a stray word | Read the message: it names the exact token and, for unknown options, lists the valid ones. Fixed in 5.1.22 — these used to crash with a raw traceback (pre-5.1.22) |
|
|
105
105
|
| ``schedule list: unexpected "…" — `pushframe schedule list` takes no arguments`` / `schedule remove: unexpected "…" — usage: pushframe schedule remove <job>` | A stray extra word after the single argument these verbs take — exit 2, nothing listed or removed. Fixed in 5.1.25 — extras were ignored silently | `schedule list` takes no arguments; `schedule remove` takes exactly one job name |
|
|
106
106
|
| A timer "fails" repeatedly | Read the job log | `~/.local/state/pushframe/<job>.log`; the unit ends on the first failure on purpose — the next tick is the retry |
|
|
107
|
+
| `=== run 2026-10-04T00:02:59+02:00 job=nightly pushframe=5.1.26 ===` opens a job log, `=== run end rc=0 elapsed=42s ===` closes it | Every scheduled run stamps its log with its identity (when, which job, which pushframe — 5.1.26) and its outcome (exit code + duration). A missing footer means the run died by exception: the traceback and systemd's `Failed` line carry that story | Read `<job>.log` one block per run; `rc=0` = healthy. No footer → the traceback above it and `journalctl --user -u pushframe-<job>` tell the rest |
|
|
108
|
+
| ` ~ <asset-id> (taken 2020-01-01) — re-show` / ` - <asset-id> (taken <date>)` lines under the plan | 5.1.26: a scheduled delta names WHICH frame assets it touches (the shape the interactive dry-run prints) — a delta is identifiable, not just countable | Check the ids against your expectations; `google-sync --pair <name>` without `--apply` shows the same plan interactively |
|
|
109
|
+
| `<job>.log.1` (and `.log.2` … `.log.28`) appearing next to a job log | 5.1.27: past 5 MiB the log slides one generation older at run end, the oldest falling off — the live file stays bounded, weeks of history stay readable. Generations beyond a lowered keep are swept | Nothing — that is the size bound working. `PUSHFRAME_JOB_LOG_KEEP` tunes the depth (a junk value falls back to the default; a rotation failure never fails a run) |
|
|
110
|
+
| A `--sync-dir` job that never ran, with `unrecognized arguments: --scheduled` in its log (pre-5.1.27) | The unit's ExecStart passed `--scheduled` before the `sync` parser accepted it — every tick died at argparse (exit 2) before any sync | Fixed in 5.1.27 — upgrade and the existing unit works as-is; re-add it (`schedule add --sync-dir …`) to also pick up `--report-tag` (named header + rotation) |
|
|
111
|
+
| `⚠ would FAIL to start: unrecognized arguments: --nope — re-add the job …` under a job line in `pushframe status` | The unit's ExecStart uses a flag the CURRENT binary's argparse refuses — the job dies on every tick before doing anything (hand-edited unit, or a unit installed by a mismatched pushframe version). 5.1.27 | Fix the unit or re-add the job; `pushframe --version` against the unit's flags tells you which way the drift goes |
|
|
107
112
|
|
|
108
113
|
## Anything else
|
|
109
114
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# pushframe — unofficial community CLI for Aura Frames (not affiliated).
|
|
2
2
|
# Version single source for the CLI surface (REL-02); pyproject keeps the
|
|
3
3
|
# packaging metadata version and tests/test_version.py enforces lockstep.
|
|
4
|
-
__version__ = "5.1.
|
|
4
|
+
__version__ = "5.1.28"
|
|
@@ -69,7 +69,7 @@ class AssetApi(BaseApi):
|
|
|
69
69
|
|
|
70
70
|
json_response = self._client.put(f'/assets/batch_update.json', data={
|
|
71
71
|
"assets": [
|
|
72
|
-
item.
|
|
72
|
+
item.model_dump(
|
|
73
73
|
include={
|
|
74
74
|
'data_uti': True,
|
|
75
75
|
'favorite': True,
|
|
@@ -179,7 +179,7 @@ class AssetApi(BaseApi):
|
|
|
179
179
|
:param asset: Asset containing new rotation/rect data.
|
|
180
180
|
:return: The asset with modified crop fields.
|
|
181
181
|
"""
|
|
182
|
-
json_response = self._client.post(f'/assets/crop.json', data=asset.
|
|
182
|
+
json_response = self._client.post(f'/assets/crop.json', data=asset.model_dump(
|
|
183
183
|
include={
|
|
184
184
|
'id': True,
|
|
185
185
|
'local_identifier': True,
|
|
@@ -145,7 +145,7 @@ class FrameApi(BaseApi):
|
|
|
145
145
|
:return: Returns the hydrated frame with changes.
|
|
146
146
|
"""
|
|
147
147
|
json_response = self._client.put(f'/frames/{frame_id}.json',
|
|
148
|
-
data={'frame': frame_partial.
|
|
148
|
+
data={'frame': frame_partial.model_dump(exclude_unset=True)})
|
|
149
149
|
return Frame(**json_response.get('frame'))
|
|
150
150
|
|
|
151
151
|
def select_asset(self, frame_id: str, asset_partial_ids: AssetPartialId | list[AssetPartialId]) -> int:
|
|
@@ -103,6 +103,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
103
103
|
sync_parser.add_argument('--frame', required=True, help='Frame name (substring) or id')
|
|
104
104
|
sync_parser.add_argument('--apply', action='store_true', default=False, help='Execute the plan (upload + delete) instead of only printing it')
|
|
105
105
|
sync_parser.add_argument('--yes', action='store_true', default=False, help='Skip the confirmation prompt (required for --apply when running non-interactively)')
|
|
106
|
+
# Scheduled sync-dir jobs (pushframe schedule add ... --sync-dir) run
|
|
107
|
+
# through this verb, so it needs the same bookkeeping flags as scheduled
|
|
108
|
+
# google-sync. (Regression: units passed --scheduled before this parser
|
|
109
|
+
# accepted it -- every sync-dir tick died 'unrecognized arguments'.)
|
|
110
|
+
sync_parser.add_argument('--scheduled', action='store_true', default=False,
|
|
111
|
+
help='Timed-run bookkeeping: the job log opens with a '
|
|
112
|
+
'timestamped "=== run ... ===" header and closes with '
|
|
113
|
+
'the rc/elapsed footer (used by pushframe schedule units)')
|
|
114
|
+
sync_parser.add_argument('--report-tag', default=None, dest='report_tag',
|
|
115
|
+
help='Job identity in the log header and the log-rotation '
|
|
116
|
+
'target (schedule units pass the job name; sync jobs '
|
|
117
|
+
'have no --report email)')
|
|
106
118
|
# The three tiers of "no longer in the local directory" (D-02/D-03).
|
|
107
119
|
# Hiding is the default because the frame has no photo-count limit, so a
|
|
108
120
|
# mistaken sync should cost visibility, never photos. argparse enforces
|
|
@@ -136,6 +148,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
136
148
|
push_parser.add_argument('--no-wait', action='store_true', default=False, help='Stop immediately instead of waiting when the write budget is exhausted')
|
|
137
149
|
push_parser.add_argument('--country', default=None, help='Override the expected account country for the geo pre-flight guard (default from PUSHFRAME_COUNTRY, legacy AURA_COUNTRY)')
|
|
138
150
|
push_parser.add_argument('--ignore-budget', action='store_true', default=False, dest='ignore_budget', help='Escape hatch: bypass the write budget entirely for this run')
|
|
151
|
+
# Scheduled push jobs (cron or hand-written units feeding a "buffet"
|
|
152
|
+
# directory) get the same log bookkeeping as scheduled sync/google-sync.
|
|
153
|
+
push_parser.add_argument('--scheduled', action='store_true', default=False,
|
|
154
|
+
help='Timed-run bookkeeping: the job log opens with a '
|
|
155
|
+
'timestamped "=== run ... ===" header and closes with '
|
|
156
|
+
'the rc/elapsed footer')
|
|
157
|
+
push_parser.add_argument('--report-tag', default=None, dest='report_tag',
|
|
158
|
+
help='Job identity in the log header and the log-rotation '
|
|
159
|
+
'target (crons and units pass a job name)')
|
|
139
160
|
|
|
140
161
|
# `reconcile` = data hygiene on EXISTING stuck placeholder rows (REL-05,
|
|
141
162
|
# D-13) -- deliberately outside the sync/push loop. Report-only by
|
|
@@ -1076,6 +1097,7 @@ def _schedule_status_section() -> list[str]:
|
|
|
1076
1097
|
for svc_path in services:
|
|
1077
1098
|
job = svc_path.stem.replace('pushframe-', '')
|
|
1078
1099
|
what = '(unreadable unit)'
|
|
1100
|
+
exec_line = ''
|
|
1079
1101
|
try:
|
|
1080
1102
|
unit_text = svc_path.read_text(encoding='utf-8')
|
|
1081
1103
|
exec_line = next((l for l in unit_text.splitlines()
|
|
@@ -1085,10 +1107,64 @@ def _schedule_status_section() -> list[str]:
|
|
|
1085
1107
|
pass
|
|
1086
1108
|
when = next_fire.get(f'pushframe-{job}.timer', '(timer not enabled)')
|
|
1087
1109
|
lines.append(f' {job}: {what} — next: {when}')
|
|
1110
|
+
if exec_line:
|
|
1111
|
+
try:
|
|
1112
|
+
problem = _unit_argv_parse_problem(exec_line)
|
|
1113
|
+
except Exception:
|
|
1114
|
+
problem = None # status degrades; it never fails here
|
|
1115
|
+
if problem:
|
|
1116
|
+
lines.append(f' ⚠ would FAIL to start: {problem} — '
|
|
1117
|
+
f're-add the job (`pushframe schedule add ...`) '
|
|
1118
|
+
f'or fix the unit')
|
|
1088
1119
|
lines.append(' (detail: `pushframe schedule list`)')
|
|
1089
1120
|
return lines
|
|
1090
1121
|
|
|
1091
1122
|
|
|
1123
|
+
def _unit_argv_parse_problem(exec_line: str) -> str | None:
|
|
1124
|
+
"""Would this unit's ExecStart survive the CURRENT binary's argparse?
|
|
1125
|
+
|
|
1126
|
+
The tokens from the verb onward go through the real parser
|
|
1127
|
+
(parse_known_args): unrecognized flags, missing required arguments or
|
|
1128
|
+
invalid values surface here as a one-line reason — None when the
|
|
1129
|
+
command line is accepted verbatim. This is the guard against the
|
|
1130
|
+
sync-dir class of breakage (units installed passing a flag the parser
|
|
1131
|
+
of the day rejects die on EVERY tick — argparse exit 2, before any
|
|
1132
|
+
sync — with nothing but journalctl to explain why): hand-edited units
|
|
1133
|
+
and version drift (unit newer or older than the binary) are exactly
|
|
1134
|
+
what it surfaces. Never raises — status degrades, it does not fail.
|
|
1135
|
+
"""
|
|
1136
|
+
import contextlib
|
|
1137
|
+
import io
|
|
1138
|
+
import shlex
|
|
1139
|
+
body = exec_line.removeprefix('ExecStart=')
|
|
1140
|
+
try:
|
|
1141
|
+
tokens = shlex.split(body)
|
|
1142
|
+
except ValueError:
|
|
1143
|
+
return 'ExecStart is not shell-quoted correctly'
|
|
1144
|
+
parser = build_parser()
|
|
1145
|
+
try:
|
|
1146
|
+
sub = next(a for a in parser._actions if a.dest == 'command')
|
|
1147
|
+
verbs = set(sub.choices)
|
|
1148
|
+
except Exception:
|
|
1149
|
+
return None # cannot introspect the parser — refuse to guess
|
|
1150
|
+
verb_idx = next((i for i, t in enumerate(tokens) if t in verbs), None)
|
|
1151
|
+
if verb_idx is None:
|
|
1152
|
+
return 'no pushframe command found in ExecStart'
|
|
1153
|
+
argv = tokens[verb_idx:]
|
|
1154
|
+
err = io.StringIO()
|
|
1155
|
+
try:
|
|
1156
|
+
with contextlib.redirect_stderr(err):
|
|
1157
|
+
_, extras = parser.parse_known_args(argv)
|
|
1158
|
+
except SystemExit:
|
|
1159
|
+
detail = err.getvalue().split('error:', 1)[-1]
|
|
1160
|
+
detail = ' '.join(detail.split())[:160]
|
|
1161
|
+
return f'argparse refused: {detail}' if detail \
|
|
1162
|
+
else 'argparse refused the command line'
|
|
1163
|
+
if extras:
|
|
1164
|
+
return f'unrecognized arguments: {" ".join(map(str, extras))[:160]}'
|
|
1165
|
+
return None
|
|
1166
|
+
|
|
1167
|
+
|
|
1092
1168
|
def _frame_flag_needs_value(rest: list[str], flag: str) -> bool:
|
|
1093
1169
|
"""True when `flag` is present but dangling (last token, or followed by
|
|
1094
1170
|
another --flag) — the ExecStart parser's guard against indexing past
|
|
@@ -2212,23 +2288,68 @@ def _scheduled_run_footer(rc: int, elapsed_s: float) -> None:
|
|
|
2212
2288
|
print(f'=== run end rc={rc} elapsed={round(elapsed_s)}s ===')
|
|
2213
2289
|
|
|
2214
2290
|
|
|
2291
|
+
JOB_LOG_KEEP_GENERATIONS = 28
|
|
2292
|
+
|
|
2293
|
+
|
|
2294
|
+
def _job_log_keep() -> int:
|
|
2295
|
+
"""Ops knob for the rotation history depth: PUSHFRAME_JOB_LOG_KEEP.
|
|
2296
|
+
A junk or <1 value falls back to the default — a retention typo must
|
|
2297
|
+
never fail a run that already succeeded (this function's only contract).
|
|
2298
|
+
"""
|
|
2299
|
+
try:
|
|
2300
|
+
keep = int(os.getenv('PUSHFRAME_JOB_LOG_KEEP', ''))
|
|
2301
|
+
except ValueError:
|
|
2302
|
+
return JOB_LOG_KEEP_GENERATIONS
|
|
2303
|
+
return keep if keep >= 1 else JOB_LOG_KEEP_GENERATIONS
|
|
2304
|
+
|
|
2305
|
+
|
|
2215
2306
|
def _rotate_job_log_if_large(report_tag: str | None, *,
|
|
2216
|
-
limit_bytes: int = 5 * 1024 * 1024
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2307
|
+
limit_bytes: int = 5 * 1024 * 1024,
|
|
2308
|
+
keep: int | None = None) -> bool:
|
|
2309
|
+
"""Bound a job's append log while keeping history consultable (systemd
|
|
2310
|
+
reopens the file with O_APPEND every start): past `limit_bytes`, the log
|
|
2311
|
+
slides one generation older — <job>.log.1 … <job>.log.<keep>, the oldest
|
|
2312
|
+
generation falling off the end — so the live file stays bounded AND the
|
|
2313
|
+
previous weeks remain readable. Generations are size-bounded, not
|
|
2314
|
+
time-bounded: a nightly job crossing the limit every night keeps ~4
|
|
2315
|
+
weeks (default 28); a quiet one keeps months. Worst case per job:
|
|
2316
|
+
keep × limit bytes. Called at run END: this process's still-open stdout
|
|
2317
|
+
fd keeps appending to the renamed inode until exit, and the next start
|
|
2318
|
+
opens a fresh <job>.log, so nothing is lost and no fd surgery is needed.
|
|
2319
|
+
A rotation failure must never fail a run that already succeeded.
|
|
2223
2320
|
"""
|
|
2224
2321
|
if not report_tag:
|
|
2225
2322
|
return False
|
|
2323
|
+
if keep is None:
|
|
2324
|
+
keep = _job_log_keep()
|
|
2325
|
+
if keep < 1:
|
|
2326
|
+
raise ValueError(f'keep={keep!r} must be >= 1')
|
|
2226
2327
|
from pushframe.schedule import LOG_DIR
|
|
2227
2328
|
log = LOG_DIR / f'{report_tag}.log'
|
|
2228
2329
|
try:
|
|
2229
2330
|
if not log.exists() or log.stat().st_size <= limit_bytes:
|
|
2230
2331
|
return False
|
|
2332
|
+
# Slide every existing generation one slot older, oldest falling off
|
|
2333
|
+
# (os.replace overwrites the destination): .log.(keep-1) -> .log.keep,
|
|
2334
|
+
# ..., .log.1 -> .log.2, then the live log -> .log.1.
|
|
2335
|
+
for slot in range(keep - 1, 0, -1):
|
|
2336
|
+
src = log.parent / f'{log.name}.{slot}'
|
|
2337
|
+
if src.exists():
|
|
2338
|
+
os.replace(src, log.parent / f'{log.name}.{slot + 1}')
|
|
2231
2339
|
os.replace(log, log.parent / (log.name + '.1'))
|
|
2340
|
+
# Generations beyond `keep` can only exist after an operator LOWERED
|
|
2341
|
+
# the retention (env or code); leftovers would linger as unbounded
|
|
2342
|
+
# silent tails, so sweep them — tolerantly, same run-success rule.
|
|
2343
|
+
try:
|
|
2344
|
+
prefix = log.name + '.'
|
|
2345
|
+
for stale in log.parent.iterdir():
|
|
2346
|
+
if not stale.name.startswith(prefix):
|
|
2347
|
+
continue
|
|
2348
|
+
suffix = stale.name[len(prefix):]
|
|
2349
|
+
if suffix.isdigit() and int(suffix) > keep:
|
|
2350
|
+
stale.unlink()
|
|
2351
|
+
except OSError:
|
|
2352
|
+
pass
|
|
2232
2353
|
return True
|
|
2233
2354
|
except OSError:
|
|
2234
2355
|
return False
|
|
@@ -2398,16 +2519,40 @@ def _main(argv=None) -> int:
|
|
|
2398
2519
|
return run_inspect(args.frame, debug=args.debug)
|
|
2399
2520
|
if args.command == 'sync':
|
|
2400
2521
|
removal_mode = 'hard_delete' if args.hard_delete else ('delete' if args.delete else 'hide')
|
|
2401
|
-
|
|
2402
|
-
|
|
2522
|
+
import time as _time
|
|
2523
|
+
if args.scheduled:
|
|
2524
|
+
_scheduled_run_header(args.report_tag)
|
|
2525
|
+
_t0 = _time.monotonic()
|
|
2526
|
+
rc = run_sync(args.dir, args.frame, apply=args.apply, yes=args.yes, debug=args.debug,
|
|
2527
|
+
removal_mode=removal_mode)
|
|
2528
|
+
if args.scheduled:
|
|
2529
|
+
# Same bookkeeping contract as scheduled google-sync: the job
|
|
2530
|
+
# log carries the run's identity (header above), end (rc +
|
|
2531
|
+
# elapsed), then the size bound — <job>.log alone answers which
|
|
2532
|
+
# night synced what.
|
|
2533
|
+
_scheduled_run_footer(rc, _time.monotonic() - _t0)
|
|
2534
|
+
_rotate_job_log_if_large(args.report_tag)
|
|
2535
|
+
return rc
|
|
2403
2536
|
if args.command == 'push':
|
|
2404
|
-
|
|
2537
|
+
import time as _time
|
|
2538
|
+
if args.scheduled:
|
|
2539
|
+
_scheduled_run_header(args.report_tag)
|
|
2540
|
+
_t0 = _time.monotonic()
|
|
2541
|
+
rc = run_sync(
|
|
2405
2542
|
args.dir, args.frame, apply=args.apply, yes=args.yes, debug=args.debug,
|
|
2406
2543
|
no_delete=True, limit=args.limit, batch_size=args.batch_size,
|
|
2407
2544
|
chunk_delay=args.chunk_delay, verb='push',
|
|
2408
2545
|
max_wait=args.max_wait, no_wait=args.no_wait,
|
|
2409
2546
|
country=args.country, ignore_budget=args.ignore_budget,
|
|
2410
2547
|
)
|
|
2548
|
+
if args.scheduled:
|
|
2549
|
+
# Same bookkeeping contract as scheduled google-sync/sync: the
|
|
2550
|
+
# job log carries the run's identity (header above), end (rc +
|
|
2551
|
+
# elapsed), then the size bound. push is purely additive, so
|
|
2552
|
+
# there is no mass-hide guard to check — bookkeeping only.
|
|
2553
|
+
_scheduled_run_footer(rc, _time.monotonic() - _t0)
|
|
2554
|
+
_rotate_job_log_if_large(args.report_tag)
|
|
2555
|
+
return rc
|
|
2411
2556
|
if args.command == 'reconcile':
|
|
2412
2557
|
return run_reconcile(
|
|
2413
2558
|
args.frame, remove=args.remove, yes=args.yes, mechanism=args.mechanism,
|
|
@@ -132,8 +132,12 @@ def _exec_start_for(pair: str | None = None, album: str | None = None,
|
|
|
132
132
|
from pushframe import config_store
|
|
133
133
|
parts = [_pushframe_command()]
|
|
134
134
|
if sync_dir:
|
|
135
|
+
# --report-tag carries the job identity into the run's log header
|
|
136
|
+
# and its rotation (sync jobs have no --report email; the tag is
|
|
137
|
+
# what makes <job>.log self-describing).
|
|
135
138
|
parts += ['sync', f'"{sync_dir}"', f'--frame "{frame}"',
|
|
136
|
-
'--apply', '--yes', '--scheduled'
|
|
139
|
+
'--apply', '--yes', '--scheduled',
|
|
140
|
+
'--report-tag', f'"{job}"']
|
|
137
141
|
else:
|
|
138
142
|
spec = (config_store.load().get('pairs', {}) or {}).get(pair or '', {})
|
|
139
143
|
album = spec.get('album', album)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
AURA_DT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_aura_dt(aura_dt_str: str):
|
|
7
|
+
return datetime.strptime(aura_dt_str, AURA_DT_FORMAT)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_utc_now():
|
|
11
|
+
"""Naive UTC now -- the exact value `datetime.utcnow()` used to return.
|
|
12
|
+
|
|
13
|
+
Deliberately NAIVE, not `datetime.now(timezone.utc)`: every consumer
|
|
14
|
+
subtracts this from naive datetimes or stores it next to them --
|
|
15
|
+
`find_placeholders` diffs it against `parse_aura_dt` results (strptime
|
|
16
|
+
is naive) and `WriteBudget.acquire` does `(now - updated_at)` arithmetic
|
|
17
|
+
on a value persisted via isoformat round-trip. One aware datetime in
|
|
18
|
+
that pipeline raises `TypeError: can't subtract offset-naive and
|
|
19
|
+
offset-aware datetimes`. `datetime.now(timezone.utc).replace(tzinfo=None)`
|
|
20
|
+
is the deprecation-free equivalent Python recommends for the old
|
|
21
|
+
`utcnow()` semantics (no wall-clock read, immune to NTP/adjtime jumps).
|
|
22
|
+
"""
|
|
23
|
+
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def format_dt_to_aura(dt: datetime):
|
|
27
|
+
return dt.strftime(AURA_DT_FORMAT)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pushframe"
|
|
3
|
-
version = "5.1.
|
|
3
|
+
version = "5.1.28"
|
|
4
4
|
description = "Unofficial community CLI for Aura Frames digital photo frames (sync a local folder or a Google Photos album; not affiliated with Aura Frames Inc.)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -21,7 +21,7 @@ from pydantic import ValidationError
|
|
|
21
21
|
|
|
22
22
|
from pushframe.models.asset import Asset, AssetPartial, AssetPartialId
|
|
23
23
|
|
|
24
|
-
# The exact allowlist AssetApi.batch_update uses for its `.
|
|
24
|
+
# The exact allowlist AssetApi.batch_update uses for its `.model_dump(include=...)`
|
|
25
25
|
# call (pushframe/api/assetApi.py:19-38) -- AssetPartial must serialize to
|
|
26
26
|
# precisely this shape.
|
|
27
27
|
BATCH_UPDATE_ALLOWLIST = {
|
|
@@ -51,7 +51,7 @@ def test_asset_partial_constructs_with_id_unset():
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def test_asset_partial_serializes_to_batch_update_payload_shape():
|
|
54
|
-
""".
|
|
54
|
+
""".model_dump(include=...) over batch_update's allowlist returns exactly
|
|
55
55
|
those keys, regardless of which fields were actually set."""
|
|
56
56
|
partial = AssetPartial(
|
|
57
57
|
local_identifier='local-id-1',
|
|
@@ -65,7 +65,7 @@ def test_asset_partial_serializes_to_batch_update_payload_shape():
|
|
|
65
65
|
upload_priority=1,
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
payload = partial.
|
|
68
|
+
payload = partial.model_dump(include=BATCH_UPDATE_ALLOWLIST)
|
|
69
69
|
|
|
70
70
|
assert set(payload.keys()) == set(BATCH_UPDATE_ALLOWLIST.keys())
|
|
71
71
|
assert payload['local_identifier'] == 'local-id-1'
|