socialcli 0.3.0__tar.gz → 0.3.1__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.
Potentially problematic release.
This version of socialcli might be problematic. Click here for more details.
- {socialcli-0.3.0 → socialcli-0.3.1}/PKG-INFO +5 -5
- {socialcli-0.3.0 → socialcli-0.3.1}/README.md +4 -4
- {socialcli-0.3.0 → socialcli-0.3.1}/pyproject.toml +1 -1
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/agent_errors.py +75 -4
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/agent_guide.py +4 -4
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/engagement.py +69 -8
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/engagement_cli.py +23 -3
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/identity.py +30 -13
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inbox.py +5 -1
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inventory.py +4 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/batches.py +20 -10
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/changes.py +30 -15
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_changes.py +4 -2
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/snippet_readback.py +9 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube_community.py +4 -4
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube_resources.py +13 -3
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_preflight.py +7 -2
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/operation_output.py +3 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/postfile.py +12 -14
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/read_reports.py +4 -1
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/connections.py +1 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/lifecycle.py +6 -3
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/pages.py +1 -1
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/pairing.py +5 -3
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/permissions.py +23 -2
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/settings.py +11 -1
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/agent-manual.md +83 -20
- socialcli-0.3.1/socialctl/ui/assets/agents.md +33 -0
- socialcli-0.3.0/socialctl/ui/assets/agents.md +0 -27
- {socialcli-0.3.0 → socialcli-0.3.1}/.gitignore +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/LICENSE +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/__init__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/__main__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/__init__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/base.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/errores.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/facebook.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/instagram.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/tiktok.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/youtube.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/approval_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/auth.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/authflow.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/brands.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/capabilities.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/capabilities.yml +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/__init__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/__main__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/app.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/PLATFORM-LOGOS-LICENSE.txt +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/connection.css +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/facebook.webp +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/instagram.svg +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/socialcli-logo.png +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/tiktok.png +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/youtube.svg +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/landing.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/legacy.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/peer.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/providers.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/settings.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/store.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/__init__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/account_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/client.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/credential_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/credential_store.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/file_store.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/keychain.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/meta_user.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/rotation_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/saas_client.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/temporary_media.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/delivery_options.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/executor.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/formatter.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/hosted_media.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inbox_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inventory_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inventory_sources.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/__init__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/batch_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/comments_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_batch_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_batches.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_schema.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/facebook_groups.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/groups_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/inbox_readers.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_changes.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_client.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_comment_inbox.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_comments.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_errors.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_pagination.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_schema.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_webhooks.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/metadata_parts.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_assets.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_changes.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_reorder.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_schema.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/reply_context.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/reply_evidence.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/resource_changes.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/resource_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/stories.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/stories_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube_metadata.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube_owned.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_ingress.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_registry.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_requirements.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/__init__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/almacen.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_changes.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_client.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_ingest.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_schema.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/base.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/facebook.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/insights.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/instagram.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/modelos.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/piezas.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/reach.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/reporting_client.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/resumen.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/slugs.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/tiktok.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/youtube.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/migration_cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/migration_files.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/models.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/__init__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/api_delivery.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/api_reconcile.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/approval.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/cadence.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/cli.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/dispatcher.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/facebook.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/facebook_reels.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/first_comment.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/handoff.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/instagram_delivery.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/instagram_runtime.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/inventory.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/migration.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/models.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/reconcile.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/remote_data.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/report.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/store.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/verification.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/youtube.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/plantillas/accounts.yml +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/plantillas/brand.md +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/plantillas/estrategia.md +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/preflight.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publication_policy.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publication_status.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publication_steps.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publish_output.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publisher.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/queue_migration.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/read_budget.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/rutas.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/__init__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/access.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/auth.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/backup.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_enrollment.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_enrollment_schema.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_inventory.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_reconcile.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_routes.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/budget.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/consent.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/mail.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/maintenance.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/meta_user.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/meta_webhooks.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/migration.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/models.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/operations.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/reminders.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/renewal.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/restore_guard.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/revocation.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/rotation.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/routes.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/store.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/temporary_media.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/webhooks.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/schedule_remote.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/schedule_sqlite.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/scheduler.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/scheduler_launchd.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/scheduler_systemd.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/source_links.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/__init__.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/components.css +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/connection-layout.css +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/otp.template.html +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/tokens.css +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/trial.template.html +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/web-layout.css +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/email/otp.html +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/email/trial.html +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/upload_consent.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/workspace.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/youtube_data.py +0 -0
- {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/youtube_data_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: socialcli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: SocialCli: publish and manage creator-owned social content from the terminal.
|
|
5
5
|
Project-URL: Homepage, https://socialcli.app
|
|
6
6
|
Project-URL: Repository, https://github.com/engels-florentino/SocialCli
|
|
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
# SocialCli
|
|
27
27
|
|
|
28
|
-
**Current source version: 0.3.
|
|
28
|
+
**Current source version: 0.3.1 (release preparation).** SocialCli SaaS is the current SocialCli
|
|
29
29
|
product. The repository root on `main` is its single development workspace;
|
|
30
30
|
the CLI, hosted connection service and account/billing components live together
|
|
31
31
|
here. Earlier versions remain in Git history for reference and migration.
|
|
@@ -61,9 +61,9 @@ Stripe uses test mode. Planned access costs
|
|
|
61
61
|
USD 10/month after 14 free days from the first successful connection. No real
|
|
62
62
|
subscriptions are collected in this pilot.
|
|
63
63
|
|
|
64
|
-
The
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
The next client release target is **0.3.1**, pending reviewed publication through
|
|
65
|
+
TestPyPI and PyPI. The currently published stable client remains **0.3.0**.
|
|
66
|
+
The hosted service is managed and verified separately from the CLI release. The client's wheel and
|
|
67
67
|
source archive are verified against the
|
|
68
68
|
reviewed build hashes before publication. Provider commercial approvals and live
|
|
69
69
|
billing cutover remain separate from package publication. See the
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# SocialCli
|
|
4
4
|
|
|
5
|
-
**Current source version: 0.3.
|
|
5
|
+
**Current source version: 0.3.1 (release preparation).** SocialCli SaaS is the current SocialCli
|
|
6
6
|
product. The repository root on `main` is its single development workspace;
|
|
7
7
|
the CLI, hosted connection service and account/billing components live together
|
|
8
8
|
here. Earlier versions remain in Git history for reference and migration.
|
|
@@ -38,9 +38,9 @@ Stripe uses test mode. Planned access costs
|
|
|
38
38
|
USD 10/month after 14 free days from the first successful connection. No real
|
|
39
39
|
subscriptions are collected in this pilot.
|
|
40
40
|
|
|
41
|
-
The
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
The next client release target is **0.3.1**, pending reviewed publication through
|
|
42
|
+
TestPyPI and PyPI. The currently published stable client remains **0.3.0**.
|
|
43
|
+
The hosted service is managed and verified separately from the CLI release. The client's wheel and
|
|
44
44
|
source archive are verified against the
|
|
45
45
|
reviewed build hashes before publication. Provider commercial approvals and live
|
|
46
46
|
billing cutover remain separate from package publication. See the
|
|
@@ -32,6 +32,10 @@ _ACTIONS = {
|
|
|
32
32
|
# Only fixed internal codes acquire action semantics. Provider text is diagnostic,
|
|
33
33
|
# never a command, an expiry signal or evidence that a write was not accepted.
|
|
34
34
|
_CATALOG = {
|
|
35
|
+
'reply_history_purged': ('user_action', 'Prior reply evidence was removed by retention. Review the preserved journal and platform before any new draft. Do not delete replay-blocking markers or repeat an uncertain write.'),
|
|
36
|
+
'dry_run_required': ('correct_input', 'inbox-draft requires --dry-run to prepare a preview. No provider content write was started.'),
|
|
37
|
+
'reply_input_invalid': ('correct_input', 'Use a version 1 reply file with a nonempty replies list (1–100 items). Each reply requires platform, account_id, comment_id and text. media_id is read from the inbox, not supplied in this file.'),
|
|
38
|
+
'target_validation_failed': ('inspect', 'Local target verification failed. Inspect the diagnostic reason; this does not establish a provider rejection or invalid credentials.'),
|
|
35
39
|
'approval_mismatch': ('user_action', 'The supplied approval does not match this exact preview. Obtain the owner approval before applying.'),
|
|
36
40
|
'approval_required': ('user_action', 'Show the complete preview and obtain explicit approval of its exact fingerprint.'),
|
|
37
41
|
'operation_cancelled': ('inspect', 'Approval was cancelled. No write was started by this command.'),
|
|
@@ -108,6 +112,7 @@ def error_document(exc: BaseException, *, read_only: bool) -> dict:
|
|
|
108
112
|
from socialctl.management.meta_schema import MetaError, MetaUncertain
|
|
109
113
|
from socialctl.metricas.base import SinPermiso
|
|
110
114
|
from socialctl.youtube_data import YouTubeDataBusy, YouTubeDataCleanupError
|
|
115
|
+
from socialctl.identity import ReadError
|
|
111
116
|
|
|
112
117
|
# A read flow may refresh a token via POST. A lost response to that operation
|
|
113
118
|
# cannot be classified as a harmless GET retry merely from the CLI command.
|
|
@@ -124,7 +129,7 @@ def error_document(exc: BaseException, *, read_only: bool) -> dict:
|
|
|
124
129
|
429:'rate_limited',503:'provider_unavailable'}.get(exc.status,'service_rejected')
|
|
125
130
|
elif isinstance(exc, (ResourceUncertain, YouTubeUpdateUncertain, CommentUncertain, MetaUncertain)):
|
|
126
131
|
code = 'operation_uncertain'
|
|
127
|
-
elif isinstance(exc, (AuthError, YouTubeManagementError, ResourceError, CommentError, MetaError)) and getattr(exc, '_socialcli_error_code', None) is not None:
|
|
132
|
+
elif isinstance(exc, (AuthError, YouTubeManagementError, ChangeError, ResourceError, CommentError, MetaError, ReadError)) and getattr(exc, '_socialcli_error_code', None) is not None:
|
|
128
133
|
code = exc._socialcli_error_code
|
|
129
134
|
elif isinstance(exc, ReadDeadlineExceeded): code = 'read_deadline'
|
|
130
135
|
elif isinstance(exc, ReadResponseTooLarge): code = 'response_too_large'
|
|
@@ -144,21 +149,66 @@ def error_document(exc: BaseException, *, read_only: bool) -> dict:
|
|
|
144
149
|
429:'rate_limited'}.get(status,'provider_unavailable' if status>=500 else 'request_rejected')
|
|
145
150
|
if type(status) is int else 'operation_rejected')
|
|
146
151
|
elif isinstance(exc, httpx.RequestError): code = 'provider_response_invalid'
|
|
152
|
+
elif isinstance(exc, ReadError):
|
|
153
|
+
code = {'credentials_missing': 'authorization_unavailable', 'credentials_expired': 'authorization_unavailable',
|
|
154
|
+
'permission_denied': 'permissions_required', 'rate_limited': 'rate_limited',
|
|
155
|
+
'timeout': 'transport_failure', 'transport_error': 'transport_failure',
|
|
156
|
+
'invalid_response': 'provider_response_invalid', 'provider_error': 'provider_unavailable',
|
|
157
|
+
'configuration_missing': 'account_not_configured'}.get(exc.code, 'target_validation_failed')
|
|
158
|
+
elif isinstance(exc, ResourceError): code = 'target_validation_failed'
|
|
147
159
|
elif isinstance(exc, ChangeError): code = 'operation_rejected'
|
|
148
160
|
elif isinstance(exc, YouTubeDataBusy): code = 'local_data_busy'
|
|
149
161
|
elif isinstance(exc, (OSError, YouTubeDataCleanupError)): code = 'storage_failure'
|
|
150
162
|
elif isinstance(exc, ValueError): code = 'invalid_local_state'
|
|
151
163
|
else: code = 'operation_failed'
|
|
152
|
-
|
|
164
|
+
result = error_for_code(code,read_only=read_only)
|
|
165
|
+
# Local validation is not evidence of a rejection by the hosted service.
|
|
166
|
+
if code == 'operation_rejected' and not isinstance(exc, ServiceRejected):
|
|
167
|
+
result['message'] = _CATALOG[code][1]
|
|
168
|
+
diagnostic = safe_diagnostic(getattr(exc, '_socialcli_diagnostic', None))
|
|
169
|
+
if isinstance(exc, ChangeError) and not diagnostic:
|
|
170
|
+
diagnostic = {'source': 'local'}
|
|
171
|
+
if isinstance(exc, ServiceRejected):
|
|
172
|
+
diagnostic.update(source='socialcli', http_status=exc.status)
|
|
173
|
+
elif isinstance(exc, httpx.HTTPStatusError):
|
|
174
|
+
diagnostic.update(http_status=exc.response.status_code, request_method=exc.request.method)
|
|
175
|
+
if diagnostic:
|
|
176
|
+
result['diagnostic'] = safe_diagnostic(diagnostic)
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def safe_diagnostic(value):
|
|
181
|
+
"""Allow only fixed machine labels, never URLs, bodies, headers or prose."""
|
|
182
|
+
if not isinstance(value, dict):
|
|
183
|
+
return {}
|
|
184
|
+
allowed = {
|
|
185
|
+
'source': {'local', 'socialcli', 'youtube'},
|
|
186
|
+
'stage': {'input_validation', 'workspace_read', 'file_read', 'credential_read', 'identity_read', 'provider_read', 'provider_write', 'target_read', 'reply_coverage', 'proposal_preparation', 'baseline_check', 'readback'},
|
|
187
|
+
'request_method': {'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'},
|
|
188
|
+
'operation': {'channels', 'videos', 'comments', 'commentThreads', 'playlistItems'},
|
|
189
|
+
'write_state': {'not_started', 'unknown', 'accepted'},
|
|
190
|
+
'content_write_state': {'not_started'},
|
|
191
|
+
'reason': {'thread_unverifiable', 'thread_identity_mismatch', 'thread_total_unverifiable', 'comment_binding_mismatch', 'parent_mismatch', 'replies_not_allowed', 'incomplete_reply_list', 'readback_mismatch'},
|
|
192
|
+
'provider_reason': {'commentsDisabled', 'quotaExceeded', 'dailyLimitExceeded', 'rateLimitExceeded', 'userRateLimitExceeded', 'insufficientPermissions', 'forbidden', 'authError', 'invalidValue', 'videoNotFound', 'commentNotFound', 'commentThreadNotFound', 'processingFailure', 'operationNotSupported'},
|
|
193
|
+
}
|
|
194
|
+
clean = {key: val for key, val in value.items()
|
|
195
|
+
if key in allowed and isinstance(val, str) and val in allowed[key]}
|
|
196
|
+
status = value.get('http_status')
|
|
197
|
+
if type(status) is int and 100 <= status <= 599:
|
|
198
|
+
clean['http_status'] = status
|
|
199
|
+
return clean
|
|
153
200
|
|
|
154
201
|
|
|
155
202
|
def with_error_code(wrapper: Exception, cause: BaseException, *, read_only: bool,
|
|
156
203
|
fallback: str | None = None) -> Exception:
|
|
157
204
|
"""Keep only a fixed code across a sanitized wrapper, never raw exception data."""
|
|
158
|
-
|
|
205
|
+
detail = error_document(cause, read_only=read_only)
|
|
206
|
+
code = detail['code']
|
|
159
207
|
if code == 'operation_failed' and fallback is not None:
|
|
160
208
|
code = error_for_code(fallback, read_only=read_only)['code']
|
|
161
209
|
wrapper._socialcli_error_code = code
|
|
210
|
+
if detail.get('diagnostic'):
|
|
211
|
+
wrapper._socialcli_diagnostic = detail['diagnostic']
|
|
162
212
|
return wrapper
|
|
163
213
|
|
|
164
214
|
|
|
@@ -166,7 +216,28 @@ def with_http_error_code(wrapper: Exception, response, *, read_only: bool) -> Ex
|
|
|
166
216
|
import httpx
|
|
167
217
|
cause = httpx.HTTPStatusError('Provider request rejected', request=response.request,
|
|
168
218
|
response=response)
|
|
169
|
-
|
|
219
|
+
wrapper = with_error_code(wrapper, cause, read_only=read_only)
|
|
220
|
+
if response.request.url.host == 'www.googleapis.com':
|
|
221
|
+
diagnostic = {'source': 'youtube', 'http_status': response.status_code,
|
|
222
|
+
'request_method': response.request.method,
|
|
223
|
+
'stage': 'provider_read' if read_only else 'provider_write',
|
|
224
|
+
'operation': response.request.url.path.rsplit('/', 1)[-1],
|
|
225
|
+
'write_state': 'not_started' if read_only else 'unknown'}
|
|
226
|
+
try:
|
|
227
|
+
reasons = response.json().get('error', {}).get('errors', [])
|
|
228
|
+
for row in reasons[:16]:
|
|
229
|
+
reason = safe_diagnostic({'provider_reason': row.get('reason')}) if isinstance(row, dict) else {}
|
|
230
|
+
if reason:
|
|
231
|
+
diagnostic.update(reason)
|
|
232
|
+
if reason['provider_reason'] in {'quotaExceeded', 'dailyLimitExceeded', 'rateLimitExceeded', 'userRateLimitExceeded'}:
|
|
233
|
+
wrapper._socialcli_error_code = 'rate_limited' if read_only else 'operation_uncertain'
|
|
234
|
+
elif reason['provider_reason'] == 'commentsDisabled' and read_only:
|
|
235
|
+
wrapper._socialcli_error_code = 'comments_disabled'
|
|
236
|
+
break
|
|
237
|
+
except (ValueError, TypeError, AttributeError):
|
|
238
|
+
pass
|
|
239
|
+
wrapper._socialcli_diagnostic = safe_diagnostic(diagnostic)
|
|
240
|
+
return wrapper
|
|
170
241
|
|
|
171
242
|
|
|
172
243
|
def emit_error(exc: Exception, *, read_only: bool) -> None:
|
|
@@ -3,11 +3,11 @@ from importlib.resources import files
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
5
|
TOPICS={
|
|
6
|
-
'setup':('Install the reviewed
|
|
6
|
+
'setup':('What agents can do','Install the reviewed release','Authorize the installation, then the social account','Persist and recover','Publication gate','Explicit approval and preflight'),
|
|
7
7
|
'publish':('Publication gate','Temporary Instagram media','Canonical publishing workflow','Check supplied media locally','Explicit approval and preflight'),
|
|
8
8
|
'schedule':('Publication gate','Temporary Instagram media','Scheduling and temporary media','Native scheduling input examples (offline)','Explicit approval and preflight'),
|
|
9
|
-
'inbox':('Publication gate','Read metrics and prepare engagement','Read diagnostics','Comment API
|
|
10
|
-
'errors':('Persist and recover','Recovery and error actions','Read diagnostics','Comment API
|
|
9
|
+
'inbox':('Publication gate','Read metrics and prepare engagement','Read diagnostics','Comment API evidence and limits','Explicit approval and preflight','Resumable batches and reply context','Purged reply history'),
|
|
10
|
+
'errors':('Persist and recover','Recovery and error actions','Read diagnostics','Comment API evidence and limits','Explicit approval and preflight','Resumable batches and reply context','Purged reply history'),
|
|
11
11
|
'permissions':('Installation connection access',),
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -20,4 +20,4 @@ def guide(topic):
|
|
|
20
20
|
sections=re.split(r'(?=^## )',full,flags=re.MULTILINE)
|
|
21
21
|
chosen=[section for section in sections if any(section.startswith('## '+heading) for heading in TOPICS[topic])]
|
|
22
22
|
return ('# SocialCli agent guide: '+topic+'\n\n'+''.join(chosen)+
|
|
23
|
-
'\nNext: socialcli agent-guide publish; socialcli agent-guide schedule; socialcli agent-guide inbox; socialcli agent-guide errors.\n')
|
|
23
|
+
'\nNext: socialcli agent-guide publish; socialcli agent-guide schedule; socialcli agent-guide inbox; socialcli agent-guide errors; socialcli agent-guide permissions; socialcli agent-guide all.\n')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Join observed comments to existing immutable reply journals; never send replies."""
|
|
2
|
+
import json
|
|
2
3
|
import uuid
|
|
3
4
|
|
|
4
5
|
from socialctl.inbox import UnifiedInboxStore
|
|
@@ -17,17 +18,25 @@ class DraftSelectionError(ChangeError):
|
|
|
17
18
|
def __init__(self, reason, message):
|
|
18
19
|
super().__init__(message)
|
|
19
20
|
self.reason = reason
|
|
21
|
+
if reason in {'reply_file_schema', 'reply_count', 'invalid_reply_selection'}:
|
|
22
|
+
self._socialcli_error_code = 'reply_input_invalid'
|
|
23
|
+
elif reason == 'reply_history_purged':
|
|
24
|
+
self._socialcli_error_code = reason
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
def draft_failure(exc, row, index, stage):
|
|
23
28
|
# Fixed local reason codes and target IDs only. Provider/storage exception
|
|
24
29
|
# text can contain credentials or private diagnostics and is not copied.
|
|
25
|
-
exc.draft_failure = {'child_index': index, 'stage': stage,
|
|
30
|
+
exc.draft_failure = {'child_index': index, 'stage': 'selection' if stage == 'selection' else 'preparation', 'step': stage,
|
|
26
31
|
**{key: row[key] for key in ('platform', 'account_id', 'comment_id', 'media_id')
|
|
27
32
|
if isinstance(row, dict) and isinstance(row.get(key), str)},
|
|
28
33
|
'reason': getattr(exc, 'reason', 'target_validation_failed' if stage == 'selection' else 'draft_preparation_failed')}
|
|
29
34
|
if isinstance(exc, (DraftSelectionError, ReplyCoverageError)):
|
|
30
35
|
exc.draft_failure['message'] = str(exc)
|
|
36
|
+
from socialctl.agent_errors import safe_diagnostic, error_document
|
|
37
|
+
write_state = 'unknown' if error_document(exc, read_only=True)['code'] == 'operation_uncertain' else 'not_started'
|
|
38
|
+
exc._socialcli_diagnostic = {**safe_diagnostic(getattr(exc, '_socialcli_diagnostic', None)),
|
|
39
|
+
'stage': stage, 'write_state': write_state, 'content_write_state': 'not_started'}
|
|
31
40
|
return exc
|
|
32
41
|
|
|
33
42
|
|
|
@@ -35,12 +44,46 @@ def http_client(budget):
|
|
|
35
44
|
return read_client(budget)
|
|
36
45
|
|
|
37
46
|
|
|
47
|
+
def purged_history(store, path):
|
|
48
|
+
"""Read only a retention marker's supplied intent, never resurrect its receipt.
|
|
49
|
+
|
|
50
|
+
Markers intentionally no longer prove account identity or a write outcome.
|
|
51
|
+
A matching parent therefore blocks a new draft conservatively across accounts;
|
|
52
|
+
an unbound marker blocks YouTube drafts, but not the read-only review.
|
|
53
|
+
"""
|
|
54
|
+
raw = json.loads(path.read_bytes())
|
|
55
|
+
if not isinstance(raw, dict) or raw.get('data_lifecycle') != 'purged':
|
|
56
|
+
return None
|
|
57
|
+
store._validate_id(path.stem)
|
|
58
|
+
if (raw.get('id') != path.stem or raw.get('platform') != 'youtube'
|
|
59
|
+
or raw.get('replay_blocked') is not True or not isinstance(raw.get('user_input'), dict)):
|
|
60
|
+
exc = ChangeError('Invalid purged journal; preserve the file for manual review.')
|
|
61
|
+
exc._socialcli_error_code = 'invalid_local_state'
|
|
62
|
+
raise exc
|
|
63
|
+
from socialctl.management.community_schema import ACTIONS, opaque_id
|
|
64
|
+
edit = raw['user_input'].get('edit')
|
|
65
|
+
action = edit.get('action') if isinstance(edit, dict) else None
|
|
66
|
+
if isinstance(action, str) and action in ACTIONS and action != 'reply':
|
|
67
|
+
return {'change_id': path.stem, 'reply': False, 'parent_id': None}
|
|
68
|
+
parent = edit.get('parent_id') if isinstance(edit, dict) and action == 'reply' else None
|
|
69
|
+
try:
|
|
70
|
+
parent = opaque_id(parent)
|
|
71
|
+
except ValueError:
|
|
72
|
+
parent = None
|
|
73
|
+
return {'change_id': path.stem, 'reply': True, 'parent_id': parent}
|
|
74
|
+
|
|
75
|
+
|
|
38
76
|
def review_inbox(brand):
|
|
39
77
|
data=UnifiedInboxStore(brand.raiz).load()
|
|
40
|
-
links={};excluded_proposals=0
|
|
78
|
+
links={};excluded_proposals=0;purged=[]
|
|
41
79
|
for provider in ('youtube-community','meta-comments'):
|
|
42
80
|
store=child_store(brand,provider)
|
|
43
81
|
for path in sorted(store.root.glob('*.json')):
|
|
82
|
+
if provider == 'youtube-community':
|
|
83
|
+
history = purged_history(store, path)
|
|
84
|
+
if history is not None:
|
|
85
|
+
purged.append(history)
|
|
86
|
+
continue
|
|
44
87
|
child=store.load(path.stem)
|
|
45
88
|
if provider=='youtube-community':
|
|
46
89
|
if child.edit['action']!='reply':continue
|
|
@@ -67,6 +110,7 @@ def review_inbox(brand):
|
|
|
67
110
|
context_key = UnifiedInboxStore.key(context)
|
|
68
111
|
links.setdefault(context_key, []).append({**links[key][-1], 'reply_parent_id':parent})
|
|
69
112
|
items=[];excluded=0
|
|
113
|
+
unbound = [p for p in purged if p['reply'] and p['parent_id'] is None]
|
|
70
114
|
for key,row in sorted(data['items'].items()):
|
|
71
115
|
field=ACCOUNT_FIELDS.get(row['platform'])
|
|
72
116
|
if not field or row['account_id']!=(brand.cuentas.get(row['platform']) or {}).get(field):
|
|
@@ -79,23 +123,32 @@ def review_inbox(brand):
|
|
|
79
123
|
states={p['status'] for p in proposals}
|
|
80
124
|
evidence=bound_evidence(row)
|
|
81
125
|
platform_replied=bool(evidence and evidence['state']=='owner_reply_found')
|
|
82
|
-
|
|
126
|
+
history = [p['change_id'] for p in purged if row['platform'] == 'youtube' and p['reply']
|
|
127
|
+
and (p['parent_id'] is None or p['parent_id'] in {row['comment_id'], row.get('parent_id')})]
|
|
128
|
+
state=('blocked' if history else 'uncertain' if states & UNCERTAIN else 'replied' if 'verified' in states or platform_replied
|
|
83
129
|
else 'draft' if 'proposed' in states else 'blocked' if states else 'observed')
|
|
84
|
-
items.append({**row,'reply_state':state,'platform_reply_state':evidence['state'] if evidence else 'unknown','proposals':proposals
|
|
130
|
+
items.append({**row,'reply_state':state,'platform_reply_state':evidence['state'] if evidence else 'unknown','proposals':proposals,
|
|
131
|
+
'history_review_required':bool(history),'purged_proposals':history})
|
|
85
132
|
return {'version':1,'brand':brand.nombre,'status':'ok','items':items,
|
|
86
133
|
'coverage':{'source':'saved_observations','live_verified':False,'complete':False,
|
|
87
134
|
'excluded_accounts':excluded,'excluded_proposals':excluded_proposals,'watermarks':data['watermarks'],
|
|
135
|
+
'purged_proposals':len(purged),'unbound_purged_proposals':len(unbound),
|
|
88
136
|
'limitations':['This is saved evidence, not a complete live inbox.',
|
|
89
137
|
'Replied means a stored verified write or scoped owner reply observed on the platform; present remote visibility is not rechecked. Unknown platform reply state never proves absence.',
|
|
138
|
+
'Purged journals remain replay-blocked. Matching or unbound reply history requires manual review before a new draft; unrelated comments remain usable.',
|
|
90
139
|
'TikTok comments are unsupported. Replying in an existing reply thread requires an explicit reply_parent_id mapping to its observed top-level comment; deeper nesting is not promised.']}}
|
|
91
140
|
|
|
92
141
|
|
|
93
142
|
def selected_replies(brand,raw):
|
|
94
143
|
if not isinstance(raw,dict) or set(raw)!={'version','replies'} or type(raw['version']) is not int or raw['version']!=1:
|
|
95
|
-
raise
|
|
144
|
+
raise DraftSelectionError('reply_file_schema', 'expected version 1 and explicit replies')
|
|
96
145
|
replies=raw['replies']
|
|
97
|
-
if not isinstance(replies,list) or not 1<=len(replies)<=100:raise
|
|
98
|
-
|
|
146
|
+
if not isinstance(replies,list) or not 1<=len(replies)<=100:raise DraftSelectionError('reply_count', 'select 1–100 explicit replies')
|
|
147
|
+
try:
|
|
148
|
+
rows={UnifiedInboxStore.key(row):row for row in review_inbox(brand)['items']}
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
exc._socialcli_diagnostic={'source':'local', 'stage':'workspace_read', 'write_state':'not_started'}
|
|
151
|
+
raise
|
|
99
152
|
result=[];seen=set()
|
|
100
153
|
for index, reply in enumerate(replies, 1):
|
|
101
154
|
try:
|
|
@@ -128,6 +181,9 @@ def selected_replies(brand,raw):
|
|
|
128
181
|
seen.add(parent_key)
|
|
129
182
|
elif reply.get('reply_parent_id'):
|
|
130
183
|
raise DraftSelectionError('reply_parent_mismatch', 'reply_parent_id is only valid for an observed nested context')
|
|
184
|
+
if row.get('history_review_required') or (source and source.get('history_review_required')):
|
|
185
|
+
raise DraftSelectionError('reply_history_purged',
|
|
186
|
+
'Prior reply evidence was removed by retention. Review the preserved journal and platform before any new draft; do not delete the replay-blocking marker.')
|
|
131
187
|
if row['reply_state'] in {'replied','uncertain'}:raise DraftSelectionError('existing_reply_state', 'existing replied/uncertain target; inspect or reconcile before another draft')
|
|
132
188
|
if row.get('author_id') == row['account_id']:
|
|
133
189
|
raise DraftSelectionError('own_comment', 'target is an own comment')
|
|
@@ -155,6 +211,7 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
|
|
|
155
211
|
selected=selected_replies(brand,raw)
|
|
156
212
|
with http_client(budget) as http:
|
|
157
213
|
for index, (row,text) in enumerate(selected, 1):
|
|
214
|
+
stage = 'target_read'
|
|
158
215
|
try:
|
|
159
216
|
budget.check()
|
|
160
217
|
if progress is not None:
|
|
@@ -168,7 +225,9 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
|
|
|
168
225
|
snippet=parent['snippet']
|
|
169
226
|
same_parent(row,{'id':parent['id'],'author_id':(snippet.get('authorChannelId') or {}).get('value'),
|
|
170
227
|
'text':snippet.get('textOriginal',snippet.get('textDisplay'))})
|
|
228
|
+
stage = 'reply_coverage'
|
|
171
229
|
check_reply_coverage(client, row)
|
|
230
|
+
stage = 'proposal_preparation'
|
|
172
231
|
child=prepare_community(client,child_store(brand,'youtube-community'),
|
|
173
232
|
dict(action='reply',video_id=row['media_id'],thread_id=row['thread_id'],parent_id=row['comment_id'],text=text),
|
|
174
233
|
reply_context=row.get('reply_context'))
|
|
@@ -178,6 +237,7 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
|
|
|
178
237
|
from socialctl.management.meta_comment_inbox import CommentInboxStore,prepare_inbox_draft
|
|
179
238
|
client=MetaCommentsClient(brand,Platform(row['platform']),http)
|
|
180
239
|
same_parent(row,client.find_comment(row['media_id'],row['comment_id']))
|
|
240
|
+
stage = 'proposal_preparation'
|
|
181
241
|
child,_=prepare_inbox_draft(client,child_store(brand,'meta-comments'),CommentInboxStore(brand.raiz),
|
|
182
242
|
media_id=row['media_id'],comment_id=row['comment_id'],text=text,
|
|
183
243
|
reply_context=row.get('reply_context'),
|
|
@@ -188,6 +248,7 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
|
|
|
188
248
|
# Recheck the exact baseline captured by preparation against the
|
|
189
249
|
# selected observation, not just the earlier network read.
|
|
190
250
|
parent=child.before['parent']
|
|
251
|
+
stage = 'baseline_check'
|
|
191
252
|
fresh=parent if provider=='meta-comments' else {'id':parent['id'],
|
|
192
253
|
'author_id':(parent['snippet'].get('authorChannelId') or {}).get('value'),
|
|
193
254
|
'text':parent['snippet'].get('textOriginal',parent['snippet'].get('textDisplay'))}
|
|
@@ -196,5 +257,5 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
|
|
|
196
257
|
progress({'child_index':index, 'total':len(selected), 'platform':row['platform'],
|
|
197
258
|
'comment_id':row['comment_id'], 'status':'prepared'})
|
|
198
259
|
except (Exception, KeyboardInterrupt) as exc:
|
|
199
|
-
raise draft_failure(exc, row, index,
|
|
260
|
+
raise draft_failure(exc, row, index, stage)
|
|
200
261
|
return prepare_community_batch(brand,prepared)
|
|
@@ -32,14 +32,34 @@ def register(app,default_root):
|
|
|
32
32
|
@app.command('inbox-draft')
|
|
33
33
|
def draft(file:Path=typer.Option(...,'--file'),brand:str=typer.Option(...,'--brand'),root:Path=typer.Option(default_root,'--root'),dry_run:bool=typer.Option(False,'--dry-run'),json_output:bool=typer.Option(False,'--json'),timeout:float=typer.Option(120,'--timeout')):
|
|
34
34
|
prepared=[]
|
|
35
|
+
stage='input_validation'
|
|
35
36
|
try:
|
|
36
|
-
if not dry_run:
|
|
37
|
+
if not dry_run:
|
|
38
|
+
exc=ChangeError('drafting requires --dry-run and complete preview before separate approval')
|
|
39
|
+
exc._socialcli_error_code='dry_run_required'
|
|
40
|
+
raise exc
|
|
41
|
+
stage='workspace_read'
|
|
37
42
|
selected=cargar_brand(root,brand)
|
|
43
|
+
stage='file_read'
|
|
38
44
|
with file.open('rb') as stream:
|
|
39
45
|
raw=stream.read(262145)
|
|
40
|
-
if len(raw)>262144:
|
|
41
|
-
|
|
46
|
+
if len(raw)>262144:
|
|
47
|
+
exc=ChangeError('reply file exceeds 256 KiB')
|
|
48
|
+
exc._socialcli_error_code='reply_input_invalid'
|
|
49
|
+
raise exc
|
|
50
|
+
stage='input_validation'
|
|
51
|
+
try:
|
|
52
|
+
document=yaml.safe_load(raw)
|
|
53
|
+
except (yaml.YAMLError, UnicodeError):
|
|
54
|
+
exc=ChangeError('reply file must be valid YAML')
|
|
55
|
+
exc._socialcli_error_code='reply_input_invalid'
|
|
56
|
+
raise exc from None
|
|
57
|
+
batch=prepare_replies(selected,document,budget=ReadBudget(timeout),prepared=prepared,progress=progress)
|
|
42
58
|
except (Exception,KeyboardInterrupt) as exc:
|
|
59
|
+
if not getattr(exc, 'draft_failure', None):
|
|
60
|
+
from socialctl.agent_errors import safe_diagnostic
|
|
61
|
+
exc._socialcli_diagnostic={'source':'local', 'stage':stage, 'write_state':'not_started',
|
|
62
|
+
**safe_diagnostic(getattr(exc, '_socialcli_diagnostic', None)), 'content_write_state':'not_started'}
|
|
43
63
|
value={'version':1,'status':'partial' if prepared else 'error','brand':brand,
|
|
44
64
|
'batch':None,'prepared':prepared,'error':error_document(exc,read_only=True)}
|
|
45
65
|
if getattr(exc, 'draft_failure', None):
|
|
@@ -79,8 +79,12 @@ class IdentityClient:
|
|
|
79
79
|
self._token = broker_access_token(brand, platform, client, refresh=False)
|
|
80
80
|
else:
|
|
81
81
|
self._token = secret.get("access_token")
|
|
82
|
-
except Exception:
|
|
83
|
-
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
from socialctl.agent_errors import error_document, with_error_code
|
|
84
|
+
detail = error_document(exc, read_only=True)
|
|
85
|
+
error = with_error_code(ReadError(detail['code']), exc, read_only=True)
|
|
86
|
+
error._socialcli_diagnostic = {**detail.get('diagnostic', {}), 'stage': 'credential_read'}
|
|
87
|
+
raise error from None
|
|
84
88
|
if not isinstance(self._token, str) or not self._token:
|
|
85
89
|
raise ReadError("credentials_missing")
|
|
86
90
|
if self.metadata["expiry"]["expired"]:
|
|
@@ -102,12 +106,20 @@ class IdentityClient:
|
|
|
102
106
|
response = self.client.get(f"{base}/{endpoint}", params=params,
|
|
103
107
|
headers={"Authorization": f"Bearer {self._token}"}, timeout=30,
|
|
104
108
|
follow_redirects=False)
|
|
105
|
-
except
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
from socialctl.agent_errors import error_document, with_error_code
|
|
111
|
+
detail = error_document(exc, read_only=True)
|
|
112
|
+
legacy_code = 'timeout' if isinstance(exc, httpx.TimeoutException) else detail['code']
|
|
113
|
+
error = with_error_code(ReadError(legacy_code), exc, read_only=True)
|
|
114
|
+
error._socialcli_diagnostic = {**detail.get('diagnostic', {}),
|
|
115
|
+
'stage': 'provider_read', 'request_method': 'GET'}
|
|
116
|
+
raise error from None
|
|
117
|
+
def rejected(code):
|
|
118
|
+
from socialctl.agent_errors import with_http_error_code
|
|
119
|
+
error = with_http_error_code(ReadError(code), response, read_only=True)
|
|
120
|
+
return error
|
|
109
121
|
if response.status_code == 429:
|
|
110
|
-
raise
|
|
122
|
+
raise rejected("rate_limited")
|
|
111
123
|
if response.status_code in {401, 403}:
|
|
112
124
|
# YouTube quota exhaustion commonly uses HTTP403.
|
|
113
125
|
try:
|
|
@@ -116,12 +128,12 @@ class IdentityClient:
|
|
|
116
128
|
except Exception:
|
|
117
129
|
reasons = []
|
|
118
130
|
if set(reasons) & {"quotaExceeded", "dailyLimitExceeded", "rateLimitExceeded", "userRateLimitExceeded"}:
|
|
119
|
-
raise
|
|
120
|
-
raise
|
|
131
|
+
raise rejected("rate_limited")
|
|
132
|
+
raise rejected("permission_denied")
|
|
121
133
|
if response.status_code == 404:
|
|
122
|
-
raise
|
|
134
|
+
raise rejected("not_found_or_inaccessible")
|
|
123
135
|
if not response.is_success:
|
|
124
|
-
raise
|
|
136
|
+
raise rejected("provider_error")
|
|
125
137
|
try:
|
|
126
138
|
data = response.json()
|
|
127
139
|
if not isinstance(data, dict):
|
|
@@ -193,8 +205,13 @@ def auth_status(brand, platform, client):
|
|
|
193
205
|
result["identity"] = probe.verify()
|
|
194
206
|
except ReadError as exc:
|
|
195
207
|
result["failure_class"] = exc.code
|
|
208
|
+
from socialctl.agent_errors import error_document
|
|
209
|
+
if getattr(exc, '_socialcli_error_code', None):
|
|
210
|
+
result['error_detail'] = error_document(exc, read_only=True)
|
|
196
211
|
if probe is not None and probe.identity is not None:
|
|
197
212
|
result["identity"] = probe.identity
|
|
198
|
-
except Exception:
|
|
199
|
-
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
from socialctl.agent_errors import error_document
|
|
215
|
+
result['error_detail'] = error_document(exc, read_only=True)
|
|
216
|
+
result["failure_class"] = result['error_detail']['code']
|
|
200
217
|
return result
|
|
@@ -80,7 +80,7 @@ class InboxInterrupted(KeyboardInterrupt):
|
|
|
80
80
|
|
|
81
81
|
def _partial_problem(report):
|
|
82
82
|
detail = annotate_partial_read({**report, 'complete': False})['error_detail']
|
|
83
|
-
return {
|
|
83
|
+
return {**detail, 'message': safe_error(report.get('error') or 'Read incomplete')}
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
def _exception_problem(exc):
|
|
@@ -90,6 +90,7 @@ def _exception_problem(exc):
|
|
|
90
90
|
def _problem_document(problem):
|
|
91
91
|
if isinstance(problem, dict):
|
|
92
92
|
return {'code': problem.get('code', 'read_partial'),
|
|
93
|
+
**({'diagnostic': problem['diagnostic']} if problem.get('diagnostic') else {}),
|
|
93
94
|
'message': safe_error(problem.get('message', 'Read incomplete'))}
|
|
94
95
|
return {'message': safe_error(str(problem))}
|
|
95
96
|
|
|
@@ -202,6 +203,9 @@ def sync_brand_inbox(brand,*,since,budget,only=None):
|
|
|
202
203
|
if resume and inventory['failure_class'] in {'invalid_cursor','provider_error'}:
|
|
203
204
|
budget.check()
|
|
204
205
|
inventory=sync_content(brand,platform,http,resume=False)
|
|
206
|
+
if inventory.get('failure_class'):
|
|
207
|
+
errors.append({'platform': 'youtube',
|
|
208
|
+
**inventory.get('error_detail', {'code': 'read_partial', 'message': 'Video inventory incomplete.'})})
|
|
205
209
|
media=[v['remote_id'] for v in InventoryStore(brand,platform,account).list_items() if v.get('last_sync_id')==inventory['sync_id']]
|
|
206
210
|
complete=inventory['complete'] and inventory['details_complete']
|
|
207
211
|
client=YouTubeInboxClient(brand,http)
|
|
@@ -103,6 +103,7 @@ def sync_content(brand, platform, client, *, max_pages=20, resume=False):
|
|
|
103
103
|
"next_cursor": None, "cursors": [], "seen_ids": [], "pages": 0,
|
|
104
104
|
"missing_resource_ids": [], "resumable": True}
|
|
105
105
|
run["failure_class"] = None
|
|
106
|
+
run.pop('error_detail', None)
|
|
106
107
|
run["identity"] = identity
|
|
107
108
|
for _ in range(max_pages):
|
|
108
109
|
requested = run["next_cursor"]
|
|
@@ -110,6 +111,8 @@ def sync_content(brand, platform, client, *, max_pages=20, resume=False):
|
|
|
110
111
|
items, following, missing, source = read_page(probe, requested)
|
|
111
112
|
except ReadError as exc:
|
|
112
113
|
run["failure_class"] = exc.code
|
|
114
|
+
from socialctl.agent_errors import error_document
|
|
115
|
+
run['error_detail'] = error_document(exc, read_only=True)
|
|
113
116
|
break
|
|
114
117
|
observed_at = now()
|
|
115
118
|
for row in items:
|
|
@@ -147,6 +150,7 @@ def sync_content(brand, platform, client, *, max_pages=20, resume=False):
|
|
|
147
150
|
return {"version": 1, "brand": brand.nombre, "platform": platform.value,
|
|
148
151
|
"account_id": probe.account_id, "identity": identity, "sync_id": run["id"],
|
|
149
152
|
"complete": run["complete"], "failure_class": run["failure_class"],
|
|
153
|
+
**({'error_detail': run['error_detail']} if run.get('failure_class') and run.get('error_detail') else {}),
|
|
150
154
|
"pagination_complete": run["complete"],
|
|
151
155
|
"details_complete": run["complete"] and not run["missing_resource_ids"],
|
|
152
156
|
"resume_available": run["resumable"], "pages": run["pages"],
|
|
@@ -258,18 +258,28 @@ def _apply_operation(client, store, batch, op):
|
|
|
258
258
|
# validation rejections get a fixed message, never arbitrary exception text.
|
|
259
259
|
diagnostic = exc.diagnostic or {"error": "Metadata validation rejected the update before PUT."}
|
|
260
260
|
_event(op, "write_rejected", **diagnostic)
|
|
261
|
-
except Exception:
|
|
261
|
+
except Exception as exc:
|
|
262
262
|
op.status = "uncertain"
|
|
263
|
-
|
|
263
|
+
from socialctl.agent_errors import error_document
|
|
264
|
+
_event(op, "write_uncertain", error_detail_json=json.dumps(error_document(exc, read_only=False), sort_keys=True))
|
|
264
265
|
else:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
266
|
+
from socialctl.management.snippet_readback import readback_attempts
|
|
267
|
+
for attempt in readback_attempts():
|
|
268
|
+
try:
|
|
269
|
+
verified = client.inspect(op.video_id)
|
|
270
|
+
mismatches = _mismatch_paths(verified, op.after)
|
|
271
|
+
op.status = "uncertain" if mismatches else "applied"
|
|
272
|
+
_event(op, "read_back_" + op.status, mismatch_paths=", ".join(mismatches), attempt=str(attempt))
|
|
273
|
+
store.save(batch)
|
|
274
|
+
if not mismatches:
|
|
275
|
+
break
|
|
276
|
+
except Exception as exc:
|
|
277
|
+
from socialctl.agent_errors import error_document
|
|
278
|
+
detail = error_document(exc, read_only=True)
|
|
279
|
+
op.status = "uncertain"
|
|
280
|
+
_event(op, "read_back_unavailable", error_detail_json=json.dumps(detail, sort_keys=True), attempt=str(attempt))
|
|
281
|
+
if not detail['retryable']:
|
|
282
|
+
break
|
|
273
283
|
store.save(batch)
|
|
274
284
|
|
|
275
285
|
|