procesio-cli 0.1.1__py3-none-any.whl
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.
- procesio_cli/__init__.py +18 -0
- procesio_cli/_cli.py +71 -0
- procesio_cli/agents/__init__.py +0 -0
- procesio_cli/agents/_lib/__init__.py +0 -0
- procesio_cli/agents/_lib/actiondef.py +24 -0
- procesio_cli/agents/_lib/errors.py +40 -0
- procesio_cli/agents/_lib/reversibility.py +236 -0
- procesio_cli/agents/_lib/toolrunner.py +141 -0
- procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-PLAYBOOK.md +62 -0
- procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-PROCESIO-INTEROP.md +76 -0
- procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-TROUBLESHOOTING.md +60 -0
- procesio_cli/agents/connector-builder/README.md +51 -0
- procesio_cli/agents/connector-builder/agent.yaml +61 -0
- procesio_cli/agents/connector-builder/context.py +19 -0
- procesio_cli/agents/connector-builder/handlers/__init__.py +1 -0
- procesio_cli/agents/connector-builder/handlers/checklist.py +71 -0
- procesio_cli/agents/connector-builder/handlers/guidance.py +37 -0
- procesio_cli/agents/connector-builder/handlers/nextstep.py +221 -0
- procesio_cli/agents/connector-builder/handlers/status.py +35 -0
- procesio_cli/agents/connector-builder/knowledge.py +47 -0
- procesio_cli/agents/connector-builder/main.py +93 -0
- procesio_cli/agents/connector-builder/tests/conftest.py +95 -0
- procesio_cli/agents/connector-builder/tests/test_cba_agent.py +148 -0
- procesio_cli/agents/orchestrator/__init__.py +0 -0
- procesio_cli/agents/orchestrator/bootstraplib.py +166 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/00-INDEX.md +60 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/01-PROCESSES.md +230 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/02-FORMS.md +287 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/03-INTEGRATIONS.md +384 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/04-DATABASE.md +293 -0
- procesio_cli/agents/procesio/PROCESIO-API-RELIABILITY-DOCTRINE.md +113 -0
- procesio_cli/agents/procesio/PROCESIO-BEST-PRACTICES.md +316 -0
- procesio_cli/agents/procesio/PROCESIO-BUILD-AND-TEST-PLAYBOOK.md +217 -0
- procesio_cli/agents/procesio/PROCESIO-DATASTORE.md +72 -0
- procesio_cli/agents/procesio/PROCESIO-ENVIRONMENTS.md +62 -0
- procesio_cli/agents/procesio/PROCESIO-SCHEDULING.md +47 -0
- procesio_cli/agents/procesio/PROCESIO-VISUAL-ORGANIZATION.md +175 -0
- procesio_cli/agents/procesio/README.md +97 -0
- procesio_cli/agents/procesio/__init__.py +0 -0
- procesio_cli/agents/procesio/agent.yaml +73 -0
- procesio_cli/agents/procesio/audit.py +226 -0
- procesio_cli/agents/procesio/checklist.py +135 -0
- procesio_cli/agents/procesio/context.py +19 -0
- procesio_cli/agents/procesio/handlers/__init__.py +0 -0
- procesio_cli/agents/procesio/handlers/audit.py +39 -0
- procesio_cli/agents/procesio/handlers/checklist.py +34 -0
- procesio_cli/agents/procesio/handlers/guidance.py +36 -0
- procesio_cli/agents/procesio/handlers/status.py +33 -0
- procesio_cli/agents/procesio/handlers/verify.py +39 -0
- procesio_cli/agents/procesio/knowledge.py +115 -0
- procesio_cli/agents/procesio/main.py +89 -0
- procesio_cli/agents/procesio/tests/__init__.py +0 -0
- procesio_cli/agents/procesio/tests/conftest.py +62 -0
- procesio_cli/agents/procesio/tests/test_audit.py +91 -0
- procesio_cli/agents/procesio/tests/test_checklist.py +28 -0
- procesio_cli/agents/procesio/tests/test_guidance.py +78 -0
- procesio_cli/agents/procesio/tests/test_manifest_sync.py +48 -0
- procesio_cli/agents/procesio/tests/test_verify.py +134 -0
- procesio_cli/agents/procesio/tests/test_workspace_scope.py +97 -0
- procesio_cli/agents/procesio/verifylib.py +222 -0
- procesio_cli/dashboard/README.md +117 -0
- procesio_cli/dashboard/__init__.py +18 -0
- procesio_cli/dashboard/serve.py +98 -0
- procesio_cli/dashboard/server/__init__.py +1 -0
- procesio_cli/dashboard/server/config_schemas.py +92 -0
- procesio_cli/dashboard/server/google.py +103 -0
- procesio_cli/dashboard/server/handler.py +160 -0
- procesio_cli/dashboard/server/inventory.py +213 -0
- procesio_cli/dashboard/server/jobs.py +118 -0
- procesio_cli/dashboard/server/llmtest.py +181 -0
- procesio_cli/dashboard/server/procesio_env.py +94 -0
- procesio_cli/dashboard/server/routes.py +74 -0
- procesio_cli/dashboard/server/runner.py +150 -0
- procesio_cli/dashboard/server/security.py +29 -0
- procesio_cli/dashboard/server/setup.py +211 -0
- procesio_cli/dashboard/server/validate.py +152 -0
- procesio_cli/dashboard/tests/conftest.py +16 -0
- procesio_cli/dashboard/tests/test_dashboard.py +225 -0
- procesio_cli/dashboard/tests/test_procesio_env.py +159 -0
- procesio_cli/dashboard/web/actions.js +274 -0
- procesio_cli/dashboard/web/api.js +79 -0
- procesio_cli/dashboard/web/app.js +385 -0
- procesio_cli/dashboard/web/copilot.js +72 -0
- procesio_cli/dashboard/web/google.js +121 -0
- procesio_cli/dashboard/web/index.html +67 -0
- procesio_cli/dashboard/web/procesio_env.js +187 -0
- procesio_cli/dashboard/web/styles.css +207 -0
- procesio_cli/registry.py +370 -0
- procesio_cli/scripts/build-router.py +249 -0
- procesio_cli/scripts/build-tool-skill.py +202 -0
- procesio_cli/scripts/check-requirements.py +131 -0
- procesio_cli/scripts/check-skill-governance.py +174 -0
- procesio_cli/scripts/evaluate-skill-routing.py +217 -0
- procesio_cli/scripts/evaluate-skills.py +316 -0
- procesio_cli/scripts/get-skill.py +122 -0
- procesio_cli/scripts/list-skills.py +68 -0
- procesio_cli/scripts/list-tools.py +73 -0
- procesio_cli/scripts/run-agent.py +59 -0
- procesio_cli/scripts/run-tool.py +123 -0
- procesio_cli/scripts/secret_scan.py +584 -0
- procesio_cli/scripts/set-credential.py +126 -0
- procesio_cli/scripts/validate-skills.py +530 -0
- procesio_cli/skills/PORTFOLIO.md +134 -0
- procesio_cli/skills/__init__.py +6 -0
- procesio_cli/skills/agent-skill-engineer/SKILL.md +226 -0
- procesio_cli/skills/agent-skill-engineer/evals/evals.json +422 -0
- procesio_cli/skills/agent-skill-engineer/references/authoring-standard.md +178 -0
- procesio_cli/skills/agent-skill-engineer/references/decision-framework.md +107 -0
- procesio_cli/skills/agent-skill-engineer/references/domain-evidence-protocol.md +248 -0
- procesio_cli/skills/agent-skill-engineer/references/evaluation-standard.md +191 -0
- procesio_cli/skills/agent-skill-engineer/references/expert-lenses.md +167 -0
- procesio_cli/skills/agent-skill-engineer/references/field-gate-standard.md +231 -0
- procesio_cli/skills/agent-skill-engineer/references/optimization-protocol.md +233 -0
- procesio_cli/skills/agent-skill-engineer/references/optimizer-contract.md +268 -0
- procesio_cli/skills/agent-skill-engineer/references/portability-standard.md +99 -0
- procesio_cli/skills/agent-skill-engineer/references/research-basis.md +128 -0
- procesio_cli/skills/agent-skill-engineer/references/review-standard.md +143 -0
- procesio_cli/skills/agent-skill-engineer/scripts/audit_skill.py +959 -0
- procesio_cli/skills/agent-skill-engineer/scripts/optimize_skill.py +891 -0
- procesio_cli/skills/agent-skill-engineer/scripts/scaffold_skill.py +397 -0
- procesio_cli/skills/evals/baseline-catalog.json +14 -0
- procesio_cli/skills/evals/baseline-routing-v2.json +38 -0
- procesio_cli/skills/evals/baseline.json +37 -0
- procesio_cli/skills/evals/behavioral.json +367 -0
- procesio_cli/skills/evals/field-boundary-development-v1.json +126 -0
- procesio_cli/skills/evals/field-closeout-correction-development-v1.json +23 -0
- procesio_cli/skills/evals/field-closeout-development-v1.json +89 -0
- procesio_cli/skills/evals/portfolio-pressure-training-v1.json +83 -0
- procesio_cli/skills/evals/routing.json +363 -0
- procesio_cli/skills/evals/validation-baseline.json +4 -0
- procesio_cli/skills/procesio-cli/SKILL.md +134 -0
- procesio_cli/skills/procesio-cli/evals/evals.json +111 -0
- procesio_cli/skills/procesio-cli/evals/workflows.json +16 -0
- procesio_cli/skills/procesio-cli/references/connector-lifecycle.md +47 -0
- procesio_cli/skills/procesio-cli/references/credentials-admin.md +113 -0
- procesio_cli/skills/procesio-cli/references/data-verification.md +71 -0
- procesio_cli/skills/procesio-cli/references/documents-files.md +44 -0
- procesio_cli/skills/procesio-cli/references/form-e2e.md +57 -0
- procesio_cli/skills/procesio-cli/references/operation-contract.md +138 -0
- procesio_cli/skills/procesio-cli/references/process-debugging.md +65 -0
- procesio_cli/skills/procesio-cli/references/process-lifecycle.md +88 -0
- procesio_cli/skills/procesio-cli/references/schedules-webhooks.md +58 -0
- procesio_cli/skills/procesio-cli/references/transport-environments.md +53 -0
- procesio_cli/skills/procesio-cli-maintainer/SKILL.md +84 -0
- procesio_cli/skills/procesio-cli-maintainer/evals/evals.json +116 -0
- procesio_cli/skills/procesio-cli-maintainer/references/change-contract.md +34 -0
- procesio_cli/skills/procesio-cli-maintainer/references/mcp-compatibility.md +13 -0
- procesio_cli/skills/procesio-cli-maintainer/references/skill-authoring.md +51 -0
- procesio_cli/skills/procesio-platform-advisor/SKILL.md +82 -0
- procesio_cli/skills/procesio-platform-advisor/evals/evals.json +116 -0
- procesio_cli/skills/procesio-platform-advisor/references/capability-map.md +22 -0
- procesio_cli/skills/procesio-platform-advisor/references/fit-framework.md +35 -0
- procesio_cli/skills/procesio-platform-advisor/references/freshness-policy.md +25 -0
- procesio_cli/skills/procesio-platform-advisor/references/sizing-method.md +70 -0
- procesio_cli/skills/procesio-platform-advisor/scripts/estimate_capacity.py +99 -0
- procesio_cli/skills/procesio-setup/SKILL.md +180 -0
- procesio_cli/skills/sql-server-optimizer/SKILL.md +86 -0
- procesio_cli/skills/sql-server-optimizer/evals/evals.json +116 -0
- procesio_cli/skills/sql-server-optimizer/references/db-context.md +44 -0
- procesio_cli/skills/sql-server-optimizer/references/generic-mode.md +32 -0
- procesio_cli/skills/sql-server-optimizer/references/procesio-mode.md +40 -0
- procesio_cli/skills/sql-server-optimizer/references/sql-rules.md +43 -0
- procesio_cli/skills/sql-server-optimizer/scripts/export-indexes.sql +178 -0
- procesio_cli/skills/sql-server-optimizer/scripts/export-procs-and-functions.sql +118 -0
- procesio_cli/skills/sql-server-optimizer/scripts/export-tables.sql +367 -0
- procesio_cli/skills/tests/__init__.py +0 -0
- procesio_cli/skills/tests/test_agent_skill_engineer.py +240 -0
- procesio_cli/skills/tests/test_auth_failure_stop.py +25 -0
- procesio_cli/skills/tests/test_capacity_estimator.py +85 -0
- procesio_cli/skills/tests/test_field_boundary_learning.py +100 -0
- procesio_cli/skills/tests/test_field_closeout_learning.py +123 -0
- procesio_cli/skills/tests/test_field_gate_standard.py +76 -0
- procesio_cli/skills/tests/test_operational_playbooks.py +51 -0
- procesio_cli/skills/tests/test_portfolio_pressure_training.py +35 -0
- procesio_cli/skills/tests/test_process_lifecycle_single_run.py +25 -0
- procesio_cli/skills/tests/test_skill_evaluator.py +67 -0
- procesio_cli/skills/tests/test_skill_optimizer.py +473 -0
- procesio_cli/skills/tests/test_skill_portfolio_review.py +70 -0
- procesio_cli/skills/tests/test_skill_release_binding.py +204 -0
- procesio_cli/skills/tests/test_skill_routing.py +74 -0
- procesio_cli/skills/tests/test_skill_scaffold_v2.py +116 -0
- procesio_cli/skills/tests/test_validate_skills.py +115 -0
- procesio_cli/tools/__init__.py +0 -0
- procesio_cli/tools/_lib/README.md +119 -0
- procesio_cli/tools/_lib/__init__.py +0 -0
- procesio_cli/tools/_lib/accounting.py +120 -0
- procesio_cli/tools/_lib/contacts.py +544 -0
- procesio_cli/tools/_lib/creds.py +83 -0
- procesio_cli/tools/_lib/creds_backends.py +578 -0
- procesio_cli/tools/_lib/dbprofiles.py +147 -0
- procesio_cli/tools/_lib/dbquery.py +286 -0
- procesio_cli/tools/_lib/dbschema.py +225 -0
- procesio_cli/tools/_lib/io.py +54 -0
- procesio_cli/tools/_lib/manifest.py +297 -0
- procesio_cli/tools/_lib/skill_release.py +127 -0
- procesio_cli/tools/_lib/skill_resources.py +118 -0
- procesio_cli/tools/_lib/userdata.py +228 -0
- procesio_cli/tools/_lib/webserialize/__init__.py +16 -0
- procesio_cli/tools/_lib/webserialize/admin.py +61 -0
- procesio_cli/tools/_lib/webserialize/client.py +264 -0
- procesio_cli/tools/_lib/webserialize/config.py +96 -0
- procesio_cli/tools/_lib/webserialize/errors.py +27 -0
- procesio_cli/tools/_lib/webserialize/framing.py +45 -0
- procesio_cli/tools/_lib/webserialize/registry.py +47 -0
- procesio_cli/tools/_lib/webserialize/runtime.py +61 -0
- procesio_cli/tools/_lib/webserialize/server.py +510 -0
- procesio_cli/tools/_lib/websession.py +194 -0
- procesio_cli/tools/connector-builder/CONNECTOR-BUILDER-API-NOTES.md +91 -0
- procesio_cli/tools/connector-builder/README.md +101 -0
- procesio_cli/tools/connector-builder/SKILL.md +248 -0
- procesio_cli/tools/connector-builder/__init__.py +7 -0
- procesio_cli/tools/connector-builder/actiondef.py +24 -0
- procesio_cli/tools/connector-builder/client.py +251 -0
- procesio_cli/tools/connector-builder/errors.py +44 -0
- procesio_cli/tools/connector-builder/gen_manifest.py +142 -0
- procesio_cli/tools/connector-builder/handlers/__init__.py +1 -0
- procesio_cli/tools/connector-builder/handlers/admin.py +182 -0
- procesio_cli/tools/connector-builder/handlers/auth.py +74 -0
- procesio_cli/tools/connector-builder/handlers/builds.py +396 -0
- procesio_cli/tools/connector-builder/handlers/common.py +104 -0
- procesio_cli/tools/connector-builder/handlers/files.py +111 -0
- procesio_cli/tools/connector-builder/handlers/raw.py +38 -0
- procesio_cli/tools/connector-builder/handlers/telemetry.py +39 -0
- procesio_cli/tools/connector-builder/main.py +94 -0
- procesio_cli/tools/connector-builder/tests/conftest.py +146 -0
- procesio_cli/tools/connector-builder/tests/test_cb_client.py +86 -0
- procesio_cli/tools/connector-builder/tests/test_cb_dispatch.py +105 -0
- procesio_cli/tools/connector-builder/tests/test_cb_manifest_sync.py +76 -0
- procesio_cli/tools/connector-builder/tests/test_cb_wait.py +123 -0
- procesio_cli/tools/connector-builder/tool.yaml +671 -0
- procesio_cli/tools/framework-map/.gitattributes +2 -0
- procesio_cli/tools/framework-map/README.md +98 -0
- procesio_cli/tools/framework-map/SKILL.md +30 -0
- procesio_cli/tools/framework-map/app.js +188 -0
- procesio_cli/tools/framework-map/fm_builder.py +476 -0
- procesio_cli/tools/framework-map/fm_catalog.py +19 -0
- procesio_cli/tools/framework-map/fm_ro_catalog.py +94 -0
- procesio_cli/tools/framework-map/fm_strings.py +101 -0
- procesio_cli/tools/framework-map/main.py +95 -0
- procesio_cli/tools/framework-map/styles.css +214 -0
- procesio_cli/tools/framework-map/tests/conftest.py +33 -0
- procesio_cli/tools/framework-map/tests/test_fm_build.py +126 -0
- procesio_cli/tools/framework-map/tests/test_fm_manifest_sync.py +63 -0
- procesio_cli/tools/framework-map/tests/test_fm_store_tree.py +75 -0
- procesio_cli/tools/framework-map/tool.yaml +61 -0
- procesio_cli/tools/mysql/MOVED-TO-CENTRAL.md +29 -0
- procesio_cli/tools/mysql/README.md +196 -0
- procesio_cli/tools/mysql/SKILL.md +37 -0
- procesio_cli/tools/mysql/__init__.py +0 -0
- procesio_cli/tools/mysql/connection.py +191 -0
- procesio_cli/tools/mysql/main.py +487 -0
- procesio_cli/tools/mysql/profiles.template.json +1 -0
- procesio_cli/tools/mysql/schema.py +434 -0
- procesio_cli/tools/mysql/tests/__init__.py +0 -0
- procesio_cli/tools/mysql/tests/conftest.py +21 -0
- procesio_cli/tools/mysql/tests/test_manifest_sync.py +68 -0
- procesio_cli/tools/mysql/tests/test_mysql.py +201 -0
- procesio_cli/tools/mysql/tests/test_mysql_schema.py +337 -0
- procesio_cli/tools/mysql/tool.yaml +112 -0
- procesio_cli/tools/procesio/DTO-SUBTOOLS-NOTE.md +360 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/00-INDEX.md +67 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/01-ANATOMY.md +209 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/02-CODE-INJECTION.md +309 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/03-DOM-CONTRACT.md +370 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/04-INTERACTION-RECIPES.md +600 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/05-STEPPER-AND-MOTION.md +426 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/06-PROCESS-INTEGRATION.md +347 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/07-DEPLOY-WORKFLOW.md +294 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/08-PITFALLS.md +569 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/09-NAVIGATION-AND-HOSTING.md +308 -0
- procesio_cli/tools/procesio/PHASE4-E2E-NOTES.md +594 -0
- procesio_cli/tools/procesio/PROCESIO-API-CORRECTIONS.md +295 -0
- procesio_cli/tools/procesio/PROCESIO-API-NOTES.md +6976 -0
- procesio_cli/tools/procesio/PROCESIO-AUTH-NOTES.md +226 -0
- procesio_cli/tools/procesio/PROCESIO-CARD-BUILD-NOTES.md +183 -0
- procesio_cli/tools/procesio/PROCESIO-CONNECTORS-NOTES.md +141 -0
- procesio_cli/tools/procesio/PROCESIO-CUSTOM-ACTION-NOTES.md +179 -0
- procesio_cli/tools/procesio/PROCESIO-DOCS-FIX-REPORT.md +123 -0
- procesio_cli/tools/procesio/PROCESIO-ENVIRONMENTS-NOTES.md +94 -0
- procesio_cli/tools/procesio/PROCESIO-FE-VALIDATION-NOTES.md +107 -0
- procesio_cli/tools/procesio/PROCESIO-FOREACH-NOTES.md +199 -0
- procesio_cli/tools/procesio/PROCESIO-FORM-API-HANG-NOTE.md +101 -0
- procesio_cli/tools/procesio/PROCESIO-FORM-SUBMISSION-NOTES.md +172 -0
- procesio_cli/tools/procesio/PROCESIO-HTML-TO-DOCX-NOTES.md +110 -0
- procesio_cli/tools/procesio/PROCESIO-INSTANCE-HISTORY-NOTES.md +242 -0
- procesio_cli/tools/procesio/PROCESIO-METERING-NOTES.md +201 -0
- procesio_cli/tools/procesio/PROCESIO-NODE-CODE-NOTES.md +279 -0
- procesio_cli/tools/procesio/PROCESIO-NODE-MODULE-WHITELIST.md +113 -0
- procesio_cli/tools/procesio/PROCESIO-PYTHON-ACTION-NOTES.md +165 -0
- procesio_cli/tools/procesio/PROCESIO-QUERY-STORE-NOTES.md +826 -0
- procesio_cli/tools/procesio/PROCESIO-RECONCILIATION-PATTERNS.md +104 -0
- procesio_cli/tools/procesio/PROCESIO-RESOURCE-MODEL-NOTES.md +1100 -0
- procesio_cli/tools/procesio/PROCESIO-SEND-EMAIL-CRED-OPTIONALITY.md +45 -0
- procesio_cli/tools/procesio/PROCESIO-SEND-EMAIL-NOTES.md +252 -0
- procesio_cli/tools/procesio/PROCESIO-SQL-ACTIONS-NOTES.md +284 -0
- procesio_cli/tools/procesio/PROCESIO-USAGE-GUIDE.md +219 -0
- procesio_cli/tools/procesio/README.md +341 -0
- procesio_cli/tools/procesio/SCHEDULE-INPUT-SECURITY-NOTES.md +63 -0
- procesio_cli/tools/procesio/SKILL.md +694 -0
- procesio_cli/tools/procesio/__init__.py +1 -0
- procesio_cli/tools/procesio/actiondef.py +27 -0
- procesio_cli/tools/procesio/assets/html2docx/html2docx.js +2155 -0
- procesio_cli/tools/procesio/auth.py +354 -0
- procesio_cli/tools/procesio/autogen.py +121 -0
- procesio_cli/tools/procesio/client.py +355 -0
- procesio_cli/tools/procesio/config.py +96 -0
- procesio_cli/tools/procesio/data/endpoints.json +2901 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/01-authentication.md +180 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/02-conventions.md +116 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/ANONYMOUS-SURFACE.md +51 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/README.md +104 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/SYMBOL-INDEX.md +444 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/03-auth-users-workspaces.md +731 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/04-processes.md +913 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/05-actions.md +605 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/06-files.md +237 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/07-forms-documents.md +985 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/08-credentials.md +532 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/09-datastore-datatypes.md +830 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/10-custom-urls.md +319 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/11-subscriptions-resources-analytics.md +455 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/12-webhooks-notifications-misc.md +519 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/13-realtime-signalr.md +153 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/99-internal-and-other-services.md +52 -0
- procesio_cli/tools/procesio/docs_info/FormBuilder mock.ts +5725 -0
- procesio_cli/tools/procesio/docs_info/Procesio Platform Actions.json +30219 -0
- procesio_cli/tools/procesio/docs_info/form-config-enums.ts +92 -0
- procesio_cli/tools/procesio/docs_info/form-theme.ts +1603 -0
- procesio_cli/tools/procesio/docs_info/process-validation-reference-1.md +2739 -0
- procesio_cli/tools/procesio/dto/__init__.py +13 -0
- procesio_cli/tools/procesio/dto/credential/builder.py +179 -0
- procesio_cli/tools/procesio/dto/credential/config.schema.json +19 -0
- procesio_cli/tools/procesio/dto/credential/description.md +193 -0
- procesio_cli/tools/procesio/dto/credential/fixtures/rest.config.json +11 -0
- procesio_cli/tools/procesio/dto/credential/fixtures/rest.dto.json +28 -0
- procesio_cli/tools/procesio/dto/credential/fixtures/rest.template.json +44 -0
- procesio_cli/tools/procesio/dto/credential/template.dto.json +16 -0
- procesio_cli/tools/procesio/dto/data/_refresh.py +147 -0
- procesio_cli/tools/procesio/dto/data/action_catalog.json +30825 -0
- procesio_cli/tools/procesio/dto/data/action_families.json +55 -0
- procesio_cli/tools/procesio/dto/data/platform_types.json +182 -0
- procesio_cli/tools/procesio/dto/datatype/builder.py +223 -0
- procesio_cli/tools/procesio/dto/datatype/config.schema.json +47 -0
- procesio_cli/tools/procesio/dto/datatype/description.md +85 -0
- procesio_cli/tools/procesio/dto/datatype/fixtures/customer.config.json +41 -0
- procesio_cli/tools/procesio/dto/datatype/fixtures/customer.dto.json +164 -0
- procesio_cli/tools/procesio/dto/datatype/template.dto.json +37 -0
- procesio_cli/tools/procesio/dto/document/builder.py +193 -0
- procesio_cli/tools/procesio/dto/document/config.schema.json +33 -0
- procesio_cli/tools/procesio/dto/document/description.md +124 -0
- procesio_cli/tools/procesio/dto/document/fixtures/invoice.config.json +16 -0
- procesio_cli/tools/procesio/dto/document/fixtures/invoice.dto.json +32 -0
- procesio_cli/tools/procesio/dto/document/template.dto.json +14 -0
- procesio_cli/tools/procesio/dto/form/FORM-STYLING-NOTES.md +142 -0
- procesio_cli/tools/procesio/dto/form/GOLDEN-DRIFT-NOTES.md +72 -0
- procesio_cli/tools/procesio/dto/form/RUN-PROCESS-RESULT-NOTES.md +80 -0
- procesio_cli/tools/procesio/dto/form/addelement.py +277 -0
- procesio_cli/tools/procesio/dto/form/builder.py +1009 -0
- procesio_cli/tools/procesio/dto/form/capture_goldens.py +106 -0
- procesio_cli/tools/procesio/dto/form/code_cipher.py +67 -0
- procesio_cli/tools/procesio/dto/form/config.schema.json +76 -0
- procesio_cli/tools/procesio/dto/form/data_shell.json +1 -0
- procesio_cli/tools/procesio/dto/form/description.md +346 -0
- procesio_cli/tools/procesio/dto/form/elements/approval.json +259 -0
- procesio_cli/tools/procesio/dto/form/elements/assignee.json +137 -0
- procesio_cli/tools/procesio/dto/form/elements/button.json +125 -0
- procesio_cli/tools/procesio/dto/form/elements/chart.json +341 -0
- procesio_cli/tools/procesio/dto/form/elements/chat.json +199 -0
- procesio_cli/tools/procesio/dto/form/elements/checkbox.json +153 -0
- procesio_cli/tools/procesio/dto/form/elements/column.json +77 -0
- procesio_cli/tools/procesio/dto/form/elements/columns.json +78 -0
- procesio_cli/tools/procesio/dto/form/elements/datetime-input.json +304 -0
- procesio_cli/tools/procesio/dto/form/elements/divider.json +54 -0
- procesio_cli/tools/procesio/dto/form/elements/dropdown.json +130 -0
- procesio_cli/tools/procesio/dto/form/elements/dynamic-table-row.json +253 -0
- procesio_cli/tools/procesio/dto/form/elements/file-upload.json +196 -0
- procesio_cli/tools/procesio/dto/form/elements/file-viewer.json +169 -0
- procesio_cli/tools/procesio/dto/form/elements/heading.json +79 -0
- procesio_cli/tools/procesio/dto/form/elements/icon.json +107 -0
- procesio_cli/tools/procesio/dto/form/elements/image.json +174 -0
- procesio_cli/tools/procesio/dto/form/elements/input.json +206 -0
- procesio_cli/tools/procesio/dto/form/elements/list.json +202 -0
- procesio_cli/tools/procesio/dto/form/elements/number-input.json +202 -0
- procesio_cli/tools/procesio/dto/form/elements/paragraph.json +79 -0
- procesio_cli/tools/procesio/dto/form/elements/radiobox.json +174 -0
- procesio_cli/tools/procesio/dto/form/elements/section.json +114 -0
- procesio_cli/tools/procesio/dto/form/elements/select.json +217 -0
- procesio_cli/tools/procesio/dto/form/elements/side-panel.json +146 -0
- procesio_cli/tools/procesio/dto/form/elements/signature-pad.json +141 -0
- procesio_cli/tools/procesio/dto/form/elements/static-table-row.json +84 -0
- procesio_cli/tools/procesio/dto/form/elements/step.json +105 -0
- procesio_cli/tools/procesio/dto/form/elements/stepper.json +126 -0
- procesio_cli/tools/procesio/dto/form/elements/tab.json +77 -0
- procesio_cli/tools/procesio/dto/form/elements/table.json +279 -0
- procesio_cli/tools/procesio/dto/form/elements/tabs.json +114 -0
- procesio_cli/tools/procesio/dto/form/elements/textarea.json +213 -0
- procesio_cli/tools/procesio/dto/form/enum_map.json +68 -0
- procesio_cli/tools/procesio/dto/form/fieldpath.py +284 -0
- procesio_cli/tools/procesio/dto/form/fixtures/reg.config.json +32 -0
- procesio_cli/tools/procesio/dto/form/fixtures/reg.elements.json +638 -0
- procesio_cli/tools/procesio/dto/form/sync_from_mock.py +386 -0
- procesio_cli/tools/procesio/dto/framework.py +125 -0
- procesio_cli/tools/procesio/dto/process/builder.py +1991 -0
- procesio_cli/tools/procesio/dto/process/config.schema.json +159 -0
- procesio_cli/tools/procesio/dto/process/description.md +218 -0
- procesio_cli/tools/procesio/dto/process/fixtures/greet.config.json +32 -0
- procesio_cli/tools/procesio/dto/process/fixtures/greet.dto.json +317 -0
- procesio_cli/tools/procesio/dto/process/naming.py +305 -0
- procesio_cli/tools/procesio/dto/process/normalize.py +74 -0
- procesio_cli/tools/procesio/dto/process/template.dto.json +32 -0
- procesio_cli/tools/procesio/dto/refdata.py +87 -0
- procesio_cli/tools/procesio/dto/registry.py +27 -0
- procesio_cli/tools/procesio/dto/webhook/builder.py +142 -0
- procesio_cli/tools/procesio/dto/webhook/config.schema.json +25 -0
- procesio_cli/tools/procesio/dto/webhook/description.md +92 -0
- procesio_cli/tools/procesio/dto/webhook/fixtures/order.config.json +10 -0
- procesio_cli/tools/procesio/dto/webhook/fixtures/order.dto.json +25 -0
- procesio_cli/tools/procesio/dto/webhook/template.dto.json +14 -0
- procesio_cli/tools/procesio/environments.py +264 -0
- procesio_cli/tools/procesio/errors.py +166 -0
- procesio_cli/tools/procesio/flowmodel/README.md +37 -0
- procesio_cli/tools/procesio/flowmodel/__init__.py +14 -0
- procesio_cli/tools/procesio/flowmodel/digest.py +223 -0
- procesio_cli/tools/procesio/flowmodel/families.py +34 -0
- procesio_cli/tools/procesio/flowmodel/fevalidation.py +1098 -0
- procesio_cli/tools/procesio/flowmodel/formdigest.py +232 -0
- procesio_cli/tools/procesio/flowmodel/inspect.py +78 -0
- procesio_cli/tools/procesio/flowmodel/model.py +82 -0
- procesio_cli/tools/procesio/flowmodel/nodeparam.py +871 -0
- procesio_cli/tools/procesio/flowmodel/reader.py +191 -0
- procesio_cli/tools/procesio/flowmodel/sqlparam.py +410 -0
- procesio_cli/tools/procesio/form_code_keys.py +50 -0
- procesio_cli/tools/procesio/formlint.py +292 -0
- procesio_cli/tools/procesio/gen_manifest.py +118 -0
- procesio_cli/tools/procesio/handlers/__init__.py +1 -0
- procesio_cli/tools/procesio/handlers/auth_actions.py +103 -0
- procesio_cli/tools/procesio/handlers/catalog.py +147 -0
- procesio_cli/tools/procesio/handlers/common.py +90 -0
- procesio_cli/tools/procesio/handlers/customaction_ops.py +146 -0
- procesio_cli/tools/procesio/handlers/datastore_ops.py +482 -0
- procesio_cli/tools/procesio/handlers/datatype_ops.py +275 -0
- procesio_cli/tools/procesio/handlers/dto_actions.py +258 -0
- procesio_cli/tools/procesio/handlers/environment_admin.py +146 -0
- procesio_cli/tools/procesio/handlers/fevalidate.py +236 -0
- procesio_cli/tools/procesio/handlers/files.py +141 -0
- procesio_cli/tools/procesio/handlers/flowgraph.py +229 -0
- procesio_cli/tools/procesio/handlers/flowlint.py +269 -0
- procesio_cli/tools/procesio/handlers/form_chains.py +336 -0
- procesio_cli/tools/procesio/handlers/form_code.py +277 -0
- procesio_cli/tools/procesio/handlers/form_element.py +199 -0
- procesio_cli/tools/procesio/handlers/form_events.py +441 -0
- procesio_cli/tools/procesio/handlers/form_update.py +270 -0
- procesio_cli/tools/procesio/handlers/formprocess.py +211 -0
- procesio_cli/tools/procesio/handlers/generic.py +135 -0
- procesio_cli/tools/procesio/handlers/layout.py +137 -0
- procesio_cli/tools/procesio/handlers/nodeparams.py +1018 -0
- procesio_cli/tools/procesio/handlers/process_layout.py +189 -0
- procesio_cli/tools/procesio/handlers/process_naming.py +213 -0
- procesio_cli/tools/procesio/handlers/processes.py +416 -0
- procesio_cli/tools/procesio/handlers/profile_admin.py +184 -0
- procesio_cli/tools/procesio/handlers/resource_ops.py +447 -0
- procesio_cli/tools/procesio/handlers/schedules.py +320 -0
- procesio_cli/tools/procesio/handlers/sqlactions.py +232 -0
- procesio_cli/tools/procesio/handlers/transport.py +463 -0
- procesio_cli/tools/procesio/handlers/usageguide.py +222 -0
- procesio_cli/tools/procesio/layout/README.md +196 -0
- procesio_cli/tools/procesio/layout/__init__.py +11 -0
- procesio_cli/tools/procesio/layout/adapter.py +736 -0
- procesio_cli/tools/procesio/layout/cycles.py +186 -0
- procesio_cli/tools/procesio/layout/dispatch.py +39 -0
- procesio_cli/tools/procesio/layout/elk/elk_runner.mjs +50 -0
- procesio_cli/tools/procesio/layout/elk/package-lock.json +21 -0
- procesio_cli/tools/procesio/layout/elk/package.json +11 -0
- procesio_cli/tools/procesio/layout/elk_engine.py +568 -0
- procesio_cli/tools/procesio/layout/engine.py +1311 -0
- procesio_cli/tools/procesio/layout/report.py +187 -0
- procesio_cli/tools/procesio/main.py +179 -0
- procesio_cli/tools/procesio/profiles.py +193 -0
- procesio_cli/tools/procesio/reliability.py +193 -0
- procesio_cli/tools/procesio/sqlformat.py +167 -0
- procesio_cli/tools/procesio/swagger.py +51 -0
- procesio_cli/tools/procesio/tests/__init__.py +0 -0
- procesio_cli/tools/procesio/tests/conftest.py +129 -0
- procesio_cli/tools/procesio/tests/test_auth.py +200 -0
- procesio_cli/tools/procesio/tests/test_autogen.py +78 -0
- procesio_cli/tools/procesio/tests/test_builder_attrpath_sqlbind.py +180 -0
- procesio_cli/tools/procesio/tests/test_builder_custom_action_catalog.py +72 -0
- procesio_cli/tools/procesio/tests/test_client.py +114 -0
- procesio_cli/tools/procesio/tests/test_client_reliability.py +139 -0
- procesio_cli/tools/procesio/tests/test_customaction.py +103 -0
- procesio_cli/tools/procesio/tests/test_customaction_test.py +80 -0
- procesio_cli/tools/procesio/tests/test_cycles.py +69 -0
- procesio_cli/tools/procesio/tests/test_datastore.py +378 -0
- procesio_cli/tools/procesio/tests/test_datastore_node.py +94 -0
- procesio_cli/tools/procesio/tests/test_dispatch.py +170 -0
- procesio_cli/tools/procesio/tests/test_document_page_size.py +19 -0
- procesio_cli/tools/procesio/tests/test_dto.py +734 -0
- procesio_cli/tools/procesio/tests/test_edit_dryrun_preview.py +96 -0
- procesio_cli/tools/procesio/tests/test_elk_layout.py +221 -0
- procesio_cli/tools/procesio/tests/test_environments.py +214 -0
- procesio_cli/tools/procesio/tests/test_fevalidate_gate.py +104 -0
- procesio_cli/tools/procesio/tests/test_fevalidation.py +317 -0
- procesio_cli/tools/procesio/tests/test_fieldpath_variables.py +87 -0
- procesio_cli/tools/procesio/tests/test_files.py +72 -0
- procesio_cli/tools/procesio/tests/test_flow_digest.py +202 -0
- procesio_cli/tools/procesio/tests/test_flowlint.py +114 -0
- procesio_cli/tools/procesio/tests/test_flowmodel.py +158 -0
- procesio_cli/tools/procesio/tests/test_form_add_element.py +256 -0
- procesio_cli/tools/procesio/tests/test_form_approval_gaps.py +319 -0
- procesio_cli/tools/procesio/tests/test_form_auto_names.py +60 -0
- procesio_cli/tools/procesio/tests/test_form_chains.py +217 -0
- procesio_cli/tools/procesio/tests/test_form_code_actions.py +188 -0
- procesio_cli/tools/procesio/tests/test_form_code_keys.py +92 -0
- procesio_cli/tools/procesio/tests/test_form_code_style.py +178 -0
- procesio_cli/tools/procesio/tests/test_form_delete_element.py +99 -0
- procesio_cli/tools/procesio/tests/test_form_element_config.py +141 -0
- procesio_cli/tools/procesio/tests/test_form_event_maps.py +135 -0
- procesio_cli/tools/procesio/tests/test_form_event_ops.py +376 -0
- procesio_cli/tools/procesio/tests/test_form_event_replace.py +86 -0
- procesio_cli/tools/procesio/tests/test_form_events.py +92 -0
- procesio_cli/tools/procesio/tests/test_form_parity.py +171 -0
- procesio_cli/tools/procesio/tests/test_form_select_source.py +74 -0
- procesio_cli/tools/procesio/tests/test_form_sync.py +35 -0
- procesio_cli/tools/procesio/tests/test_form_update.py +133 -0
- procesio_cli/tools/procesio/tests/test_formlint.py +155 -0
- procesio_cli/tools/procesio/tests/test_formlint_css_scope.py +38 -0
- procesio_cli/tools/procesio/tests/test_formlint_field_names.py +83 -0
- procesio_cli/tools/procesio/tests/test_guards.py +110 -0
- procesio_cli/tools/procesio/tests/test_hidden_setting_types.py +39 -0
- procesio_cli/tools/procesio/tests/test_html2docx_asset.py +132 -0
- procesio_cli/tools/procesio/tests/test_key_handle_terminology.py +45 -0
- procesio_cli/tools/procesio/tests/test_layout.py +574 -0
- procesio_cli/tools/procesio/tests/test_layout_report.py +88 -0
- procesio_cli/tools/procesio/tests/test_manifest_sync.py +68 -0
- procesio_cli/tools/procesio/tests/test_nodedelete.py +151 -0
- procesio_cli/tools/procesio/tests/test_nodeparam.py +826 -0
- procesio_cli/tools/procesio/tests/test_normalize.py +70 -0
- procesio_cli/tools/procesio/tests/test_payload_docs.py +86 -0
- procesio_cli/tools/procesio/tests/test_process_action_naming.py +170 -0
- procesio_cli/tools/procesio/tests/test_process_call_api_seed.py +46 -0
- procesio_cli/tools/procesio/tests/test_process_ext.py +373 -0
- procesio_cli/tools/procesio/tests/test_process_invariants.py +183 -0
- procesio_cli/tools/procesio/tests/test_process_layout.py +117 -0
- procesio_cli/tools/procesio/tests/test_process_naming.py +188 -0
- procesio_cli/tools/procesio/tests/test_process_query_store.py +172 -0
- procesio_cli/tools/procesio/tests/test_processinfo_variable.py +56 -0
- procesio_cli/tools/procesio/tests/test_profiles.py +127 -0
- procesio_cli/tools/procesio/tests/test_put_projects.py +41 -0
- procesio_cli/tools/procesio/tests/test_reliability.py +107 -0
- procesio_cli/tools/procesio/tests/test_removal_gate.py +95 -0
- procesio_cli/tools/procesio/tests/test_resource_ops.py +173 -0
- procesio_cli/tools/procesio/tests/test_save_validity.py +207 -0
- procesio_cli/tools/procesio/tests/test_schedule_cron_shape.py +111 -0
- procesio_cli/tools/procesio/tests/test_schedules.py +309 -0
- procesio_cli/tools/procesio/tests/test_sqlconvert.py +234 -0
- procesio_cli/tools/procesio/tests/test_sqlformat.py +83 -0
- procesio_cli/tools/procesio/tests/test_sqlparam.py +187 -0
- procesio_cli/tools/procesio/tests/test_superseded_actions.py +54 -0
- procesio_cli/tools/procesio/tests/test_surgical_concurrency.py +156 -0
- procesio_cli/tools/procesio/tests/test_tier4.py +123 -0
- procesio_cli/tools/procesio/tests/test_transport.py +147 -0
- procesio_cli/tools/procesio/tests/test_transport_data_stores.py +168 -0
- procesio_cli/tools/procesio/tests/test_transport_mapper_repair.py +150 -0
- procesio_cli/tools/procesio/tests/test_transport_webhook_strip.py +253 -0
- procesio_cli/tools/procesio/tests/test_usage_guide.py +96 -0
- procesio_cli/tools/procesio/tool.yaml +12486 -0
- procesio_cli/tools/sqlserver/MOVED-TO-CENTRAL.md +29 -0
- procesio_cli/tools/sqlserver/README.md +192 -0
- procesio_cli/tools/sqlserver/SKILL.md +37 -0
- procesio_cli/tools/sqlserver/SQLSERVER-SCHEMA-NOTES.md +284 -0
- procesio_cli/tools/sqlserver/__init__.py +0 -0
- procesio_cli/tools/sqlserver/connection.py +242 -0
- procesio_cli/tools/sqlserver/main.py +521 -0
- procesio_cli/tools/sqlserver/profiles.template.json +1 -0
- procesio_cli/tools/sqlserver/schema.py +600 -0
- procesio_cli/tools/sqlserver/tests/__init__.py +0 -0
- procesio_cli/tools/sqlserver/tests/conftest.py +21 -0
- procesio_cli/tools/sqlserver/tests/test_connection.py +75 -0
- procesio_cli/tools/sqlserver/tests/test_dbprofiles.py +92 -0
- procesio_cli/tools/sqlserver/tests/test_dbquery.py +220 -0
- procesio_cli/tools/sqlserver/tests/test_manifest_sync.py +68 -0
- procesio_cli/tools/sqlserver/tests/test_sqlserver.py +326 -0
- procesio_cli/tools/sqlserver/tests/test_sqlserver_schema.py +488 -0
- procesio_cli/tools/sqlserver/tool.yaml +123 -0
- procesio_cli/tools/web/README.md +381 -0
- procesio_cli/tools/web/SKILL.md +37 -0
- procesio_cli/tools/web/WEB-AUTOMATION-NOTES.md +277 -0
- procesio_cli/tools/web/__init__.py +21 -0
- procesio_cli/tools/web/actiondef.py +24 -0
- procesio_cli/tools/web/api.py +444 -0
- procesio_cli/tools/web/brokered.py +107 -0
- procesio_cli/tools/web/driver.py +839 -0
- procesio_cli/tools/web/drivercontract.py +44 -0
- procesio_cli/tools/web/errors.py +54 -0
- procesio_cli/tools/web/handlers/__init__.py +1 -0
- procesio_cli/tools/web/handlers/archive.py +367 -0
- procesio_cli/tools/web/handlers/browse.py +182 -0
- procesio_cli/tools/web/handlers/contacts.py +74 -0
- procesio_cli/tools/web/handlers/sessmgmt.py +120 -0
- procesio_cli/tools/web/main.py +128 -0
- procesio_cli/tools/web/sessions.py +253 -0
- procesio_cli/tools/web/steps.py +162 -0
- procesio_cli/tools/web/tests/__init__.py +0 -0
- procesio_cli/tools/web/tests/conftest.py +197 -0
- procesio_cli/tools/web/tests/test_api.py +330 -0
- procesio_cli/tools/web/tests/test_archive.py +190 -0
- procesio_cli/tools/web/tests/test_brokered.py +138 -0
- procesio_cli/tools/web/tests/test_dispatch.py +132 -0
- procesio_cli/tools/web/tests/test_errors.py +60 -0
- procesio_cli/tools/web/tests/test_manifest_sync.py +76 -0
- procesio_cli/tools/web/tests/test_page_contacts.py +68 -0
- procesio_cli/tools/web/tests/test_sessions.py +145 -0
- procesio_cli/tools/web/tests/test_steps.py +210 -0
- procesio_cli/tools/web/tool.yaml +184 -0
- procesio_cli/tools/xlsx/README.md +34 -0
- procesio_cli/tools/xlsx/SKILL.md +28 -0
- procesio_cli/tools/xlsx/__init__.py +0 -0
- procesio_cli/tools/xlsx/main.py +129 -0
- procesio_cli/tools/xlsx/reader.py +91 -0
- procesio_cli/tools/xlsx/tests/__init__.py +0 -0
- procesio_cli/tools/xlsx/tests/conftest.py +39 -0
- procesio_cli/tools/xlsx/tests/test_xlsx.py +103 -0
- procesio_cli/tools/xlsx/tool.yaml +30 -0
- procesio_cli/webplatform/aat_mcp/AAT-MCP-NOTES.md +244 -0
- procesio_cli/webplatform/aat_mcp/README.md +78 -0
- procesio_cli/webplatform/aat_mcp/bridge.py +705 -0
- procesio_cli/webplatform/aat_mcp/chat_server.py +194 -0
- procesio_cli/webplatform/aat_mcp/chat_surface.snapshot.json +668 -0
- procesio_cli/webplatform/aat_mcp/chat_surface.yaml +372 -0
- procesio_cli/webplatform/aat_mcp/gate.py +68 -0
- procesio_cli/webplatform/aat_mcp/http_server.py +139 -0
- procesio_cli/webplatform/aat_mcp/protocol.py +175 -0
- procesio_cli/webplatform/aat_mcp/server.py +306 -0
- procesio_cli/webplatform/aat_mcp/tests/conftest.py +8 -0
- procesio_cli/webplatform/aat_mcp/tests/test_aat_mcp.py +255 -0
- procesio_cli/webplatform/aat_mcp/tests/test_chat_surface.py +247 -0
- procesio_cli/webplatform/aat_mcp/tests/test_discovery.py +489 -0
- procesio_cli/webplatform/aat_mcp/tests/test_multiuser.py +86 -0
- procesio_cli/webplatform/aat_mcp/tests/test_progressive_discovery.py +89 -0
- procesio_cli/webplatform/aat_mcp/tests/test_protocol.py +180 -0
- procesio_cli/webplatform/aat_mcp/tests/test_skill_discovery.py +23 -0
- procesio_cli-0.1.1.dist-info/METADATA +467 -0
- procesio_cli-0.1.1.dist-info/RECORD +647 -0
- procesio_cli-0.1.1.dist-info/WHEEL +4 -0
- procesio_cli-0.1.1.dist-info/entry_points.txt +7 -0
- procesio_cli-0.1.1.dist-info/licenses/LICENSE +201 -0
- procesio_cli-0.1.1.dist-info/licenses/NOTICE +5 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
"""Browser driver abstraction for the web tool.
|
|
2
|
+
|
|
3
|
+
The whole point of this layer is testability: the declarative step interpreter
|
|
4
|
+
(``steps.py``) and the high-level API (``api.py``) talk only to the small
|
|
5
|
+
``BrowserDriver`` interface below. Tests inject a ``FakeDriver`` (see
|
|
6
|
+
``tests/conftest.py``) and run the entire tool with ZERO real browser.
|
|
7
|
+
|
|
8
|
+
``PlaywrightDriver`` is the production implementation. Playwright is imported
|
|
9
|
+
**lazily inside methods** so importing this module (and running the hermetic
|
|
10
|
+
test suite) never requires the browser binaries to be installed.
|
|
11
|
+
|
|
12
|
+
Lifecycle
|
|
13
|
+
---------
|
|
14
|
+
A driver wraps one Playwright *browser context* (optionally seeded from a saved
|
|
15
|
+
``storageState``) and one *page*. Use it as a context manager::
|
|
16
|
+
|
|
17
|
+
with PlaywrightDriver(storage_state=state, headless=True) as d:
|
|
18
|
+
d.goto("https://example.com")
|
|
19
|
+
text = d.extract_text("h1")
|
|
20
|
+
|
|
21
|
+
``__exit__`` always closes the page, context, browser, and Playwright runtime.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any, Protocol, runtime_checkable
|
|
26
|
+
|
|
27
|
+
from tools.web.errors import BrowserError
|
|
28
|
+
|
|
29
|
+
DEFAULT_TIMEOUT_MS = 15000
|
|
30
|
+
DEFAULT_NAV_TIMEOUT_MS = 30000
|
|
31
|
+
|
|
32
|
+
# Chromium flag that drops the most common automation fingerprint. Applied to
|
|
33
|
+
# every launch (harmless when already headed). Paired with the UA cleanup in
|
|
34
|
+
# ``_apply_stealth`` so consumer web apps (notably WhatsApp Web, which gates on
|
|
35
|
+
# a "HeadlessChrome" UA) load normally instead of the "update Chrome" page.
|
|
36
|
+
_STEALTH_ARGS = ("--disable-blink-features=AutomationControlled",)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@runtime_checkable
|
|
40
|
+
class BrowserDriver(Protocol):
|
|
41
|
+
"""The surface every action/step relies on. Implemented for real by
|
|
42
|
+
``PlaywrightDriver`` and faked in tests. Keep this minimal and stable -
|
|
43
|
+
other tools import against this contract."""
|
|
44
|
+
|
|
45
|
+
def goto(self, url: str, *, timeout: int | None = None) -> None: ...
|
|
46
|
+
|
|
47
|
+
def click(self, selector: str, *, timeout: int | None = None,
|
|
48
|
+
force: bool = False) -> None: ...
|
|
49
|
+
|
|
50
|
+
def hover(self, selector: str, *, timeout: int | None = None) -> None: ...
|
|
51
|
+
|
|
52
|
+
def context_click(self, selector: str, *, timeout: int | None = None) -> None: ...
|
|
53
|
+
|
|
54
|
+
def mouse_click(self, x: float, y: float, *, button: str = "left",
|
|
55
|
+
move_first: bool = True) -> None: ...
|
|
56
|
+
|
|
57
|
+
def capture_download(self, resolve_path, trigger,
|
|
58
|
+
*, timeout: int | None = None) -> dict: ...
|
|
59
|
+
|
|
60
|
+
def fill(self, selector: str, text: str, *, timeout: int | None = None) -> None: ...
|
|
61
|
+
|
|
62
|
+
def press(self, key: str, *, selector: str | None = None) -> None: ...
|
|
63
|
+
|
|
64
|
+
def type_text(self, text: str, *, selector: str | None = None,
|
|
65
|
+
timeout: int | None = None) -> None: ...
|
|
66
|
+
|
|
67
|
+
def wait(self, ms: int) -> None: ...
|
|
68
|
+
|
|
69
|
+
def wait_for(self, selector: str, *, timeout: int | None = None) -> None: ...
|
|
70
|
+
|
|
71
|
+
def extract_text(self, selector: str | None = None,
|
|
72
|
+
*, timeout: int | None = None) -> str: ...
|
|
73
|
+
|
|
74
|
+
def extract_attr(self, selector: str, attr: str,
|
|
75
|
+
*, timeout: int | None = None) -> str | None: ...
|
|
76
|
+
|
|
77
|
+
def inner_html(self, selector: str, *, timeout: int | None = None) -> str: ...
|
|
78
|
+
|
|
79
|
+
def eval_js(self, script: str, *, arg: Any = None) -> Any: ...
|
|
80
|
+
|
|
81
|
+
def set_input_files(self, selector: str, files: Any,
|
|
82
|
+
*, timeout: int | None = None) -> None: ...
|
|
83
|
+
|
|
84
|
+
def set_files_via_chooser(self, trigger_selector: str, files: Any,
|
|
85
|
+
*, timeout: int | None = None) -> None: ...
|
|
86
|
+
|
|
87
|
+
def set_viewport(self, width: int, height: int) -> None: ...
|
|
88
|
+
|
|
89
|
+
def screenshot(self, path: str, *, full_page: bool = True) -> str: ...
|
|
90
|
+
|
|
91
|
+
def render_pdf(self, html: str, path: str, *, fmt: str = "A4",
|
|
92
|
+
width: str | None = None, height: str | None = None,
|
|
93
|
+
margin: str | None = None) -> str: ...
|
|
94
|
+
|
|
95
|
+
def current_url(self) -> str: ...
|
|
96
|
+
|
|
97
|
+
def get_cookies(self) -> list: ...
|
|
98
|
+
|
|
99
|
+
def add_cookies(self, cookies: list) -> None: ...
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_PERSISTENT_STORAGE_JS = r"""
|
|
103
|
+
(() => {
|
|
104
|
+
try {
|
|
105
|
+
const sm = navigator.storage;
|
|
106
|
+
if (sm) {
|
|
107
|
+
sm.persist = () => Promise.resolve(true);
|
|
108
|
+
sm.persisted = () => Promise.resolve(true);
|
|
109
|
+
}
|
|
110
|
+
const sb = navigator.storageBuckets;
|
|
111
|
+
if (sb && sb.open) {
|
|
112
|
+
const open = sb.open.bind(sb);
|
|
113
|
+
sb.open = async (name, opts) => {
|
|
114
|
+
const bucket = await open(name, Object.assign({}, opts || {}, { persisted: false }));
|
|
115
|
+
try {
|
|
116
|
+
bucket.persist = () => Promise.resolve(true);
|
|
117
|
+
bucket.persisted = () => Promise.resolve(true);
|
|
118
|
+
} catch (e) {}
|
|
119
|
+
return bucket;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {}
|
|
123
|
+
})();
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class PlaywrightDriver:
|
|
128
|
+
"""Production ``BrowserDriver`` backed by Playwright's sync API.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
storage_state:
|
|
133
|
+
A Playwright storageState dict (from a saved session) or None for an
|
|
134
|
+
anonymous context.
|
|
135
|
+
headless:
|
|
136
|
+
Run without a visible window. Defaults True. ``save-session`` passes
|
|
137
|
+
False so the human can log in.
|
|
138
|
+
channel:
|
|
139
|
+
Optional Chromium channel ("chrome", "msedge"). When None, the bundled
|
|
140
|
+
Chromium build is used.
|
|
141
|
+
default_timeout:
|
|
142
|
+
Per-action default timeout in ms (selectors, fills, clicks).
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, *, storage_state: dict | None = None, headless: bool = True,
|
|
146
|
+
channel: str | None = None, user_data_dir: str | None = None,
|
|
147
|
+
default_timeout: int = DEFAULT_TIMEOUT_MS,
|
|
148
|
+
nav_timeout: int = DEFAULT_NAV_TIMEOUT_MS,
|
|
149
|
+
extra_args: list | None = None):
|
|
150
|
+
self._storage_state = storage_state
|
|
151
|
+
# Extra Chromium launch flags (e.g. fake-mic for voice notes).
|
|
152
|
+
self._extra_args = list(extra_args or [])
|
|
153
|
+
self._headless = headless
|
|
154
|
+
self._channel = channel
|
|
155
|
+
# When set, use a PERSISTENT context (Chromium user-data-dir) instead of
|
|
156
|
+
# launch()+new_context(storage_state). Required for IndexedDB-auth sites
|
|
157
|
+
# like WhatsApp Web. Mutually exclusive with storage_state.
|
|
158
|
+
self._user_data_dir = user_data_dir
|
|
159
|
+
self._default_timeout = default_timeout
|
|
160
|
+
self._nav_timeout = nav_timeout
|
|
161
|
+
self._pw = None
|
|
162
|
+
self._browser = None
|
|
163
|
+
self._context = None
|
|
164
|
+
self._page = None
|
|
165
|
+
# runtime diagnostics collected during a session: console errors/warnings,
|
|
166
|
+
# uncaught page errors, failed requests, and HTTP error responses (>=400).
|
|
167
|
+
# This is the observability that lets verification SEE runtime failures
|
|
168
|
+
# (a 400 launch, a JS exception) instead of only guessing from a screenshot.
|
|
169
|
+
self._diag = {"console": [], "page_errors": [], "failed_requests": [], "bad_responses": []}
|
|
170
|
+
|
|
171
|
+
# --- lifecycle --------------------------------------------------------
|
|
172
|
+
def start(self) -> "PlaywrightDriver":
|
|
173
|
+
try:
|
|
174
|
+
from playwright.sync_api import sync_playwright
|
|
175
|
+
except ImportError as exc: # pragma: no cover - env without playwright
|
|
176
|
+
raise BrowserError(
|
|
177
|
+
"playwright is not installed. Run: uv add playwright"
|
|
178
|
+
) from exc
|
|
179
|
+
try:
|
|
180
|
+
self._pw = sync_playwright().start()
|
|
181
|
+
if self._user_data_dir is not None:
|
|
182
|
+
# Persistent profile: one call returns a context (no separate
|
|
183
|
+
# browser object). The native on-disk profile carries the login.
|
|
184
|
+
ctx_kwargs: dict[str, Any] = {"headless": self._headless,
|
|
185
|
+
"args": list(_STEALTH_ARGS) + self._extra_args}
|
|
186
|
+
if self._channel:
|
|
187
|
+
ctx_kwargs["channel"] = self._channel
|
|
188
|
+
self._context = self._pw.chromium.launch_persistent_context(
|
|
189
|
+
self._user_data_dir, **ctx_kwargs)
|
|
190
|
+
self._browser = None
|
|
191
|
+
else:
|
|
192
|
+
launch_kwargs: dict[str, Any] = {"headless": self._headless,
|
|
193
|
+
"args": list(_STEALTH_ARGS) + self._extra_args}
|
|
194
|
+
if self._channel:
|
|
195
|
+
launch_kwargs["channel"] = self._channel
|
|
196
|
+
self._browser = self._pw.chromium.launch(**launch_kwargs)
|
|
197
|
+
ctx_kwargs = {}
|
|
198
|
+
if self._storage_state is not None:
|
|
199
|
+
ctx_kwargs["storage_state"] = self._storage_state
|
|
200
|
+
self._context = self._browser.new_context(**ctx_kwargs)
|
|
201
|
+
self._context.set_default_timeout(self._default_timeout)
|
|
202
|
+
self._context.set_default_navigation_timeout(self._nav_timeout)
|
|
203
|
+
# A persistent context opens with one page already; reuse it.
|
|
204
|
+
pages = self._context.pages
|
|
205
|
+
self._page = pages[0] if pages else self._context.new_page()
|
|
206
|
+
self._attach_diagnostics()
|
|
207
|
+
# Strip the headless/automation fingerprint so consumer web apps
|
|
208
|
+
# load (WhatsApp Web rejects the "HeadlessChrome" UA). Best-effort.
|
|
209
|
+
self._apply_stealth()
|
|
210
|
+
except Exception as exc: # noqa: BLE001
|
|
211
|
+
self.close()
|
|
212
|
+
raise _wrap(exc)
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
def _attach_diagnostics(self) -> None:
|
|
216
|
+
"""Listen for console errors/warnings, uncaught page errors, failed requests
|
|
217
|
+
and HTTP >=400 responses. Best-effort; capping each list keeps memory bounded."""
|
|
218
|
+
d = self._diag
|
|
219
|
+
|
|
220
|
+
def _cap(key, item):
|
|
221
|
+
if len(d[key]) < 100:
|
|
222
|
+
d[key].append(item)
|
|
223
|
+
|
|
224
|
+
def _console(msg):
|
|
225
|
+
try:
|
|
226
|
+
if msg.type in ("error", "warning"):
|
|
227
|
+
_cap("console", {"type": msg.type, "text": (msg.text or "")[:500]})
|
|
228
|
+
except Exception: # noqa: BLE001
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
def _pageerror(err):
|
|
232
|
+
try:
|
|
233
|
+
_cap("page_errors", str(err)[:500])
|
|
234
|
+
except Exception: # noqa: BLE001
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
def _reqfailed(req):
|
|
238
|
+
try:
|
|
239
|
+
_cap("failed_requests", {"url": (req.url or "")[:200], "method": req.method,
|
|
240
|
+
"failure": str(getattr(req, "failure", "") or "")[:200]})
|
|
241
|
+
except Exception: # noqa: BLE001
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
def _response(resp):
|
|
245
|
+
try:
|
|
246
|
+
if resp.status >= 400:
|
|
247
|
+
_cap("bad_responses", {"url": (resp.url or "")[:200], "status": resp.status})
|
|
248
|
+
except Exception: # noqa: BLE001
|
|
249
|
+
pass
|
|
250
|
+
try:
|
|
251
|
+
self._page.on("console", _console)
|
|
252
|
+
self._page.on("pageerror", _pageerror)
|
|
253
|
+
self._page.on("requestfailed", _reqfailed)
|
|
254
|
+
self._page.on("response", _response)
|
|
255
|
+
except Exception: # noqa: BLE001 - diagnostics must never break a launch
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
def diagnostics(self) -> dict:
|
|
259
|
+
"""Snapshot of runtime issues seen so far: console errors/warnings, uncaught
|
|
260
|
+
page errors, failed requests, HTTP >=400 responses. Empty lists = clean run."""
|
|
261
|
+
return {k: list(v) for k, v in self._diag.items()}
|
|
262
|
+
|
|
263
|
+
def _apply_stealth(self) -> None:
|
|
264
|
+
"""Make automated Chromium pass as a normal browser.
|
|
265
|
+
|
|
266
|
+
Two fingerprints break consumer web apps under automation: the
|
|
267
|
+
User-Agent (default headless Chromium reports "HeadlessChrome", which
|
|
268
|
+
WhatsApp Web's browser-support gate rejects, so it never reaches the
|
|
269
|
+
chat list) and ``navigator.webdriver``. We drop "Headless" from the UA
|
|
270
|
+
via CDP's ``Network.setUserAgentOverride`` (dynamic — no hard-coded
|
|
271
|
+
version) and shadow ``navigator.webdriver`` for future navigations.
|
|
272
|
+
Every step is guarded: stealth must never break a launch."""
|
|
273
|
+
try:
|
|
274
|
+
self._context.add_init_script(
|
|
275
|
+
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
|
276
|
+
)
|
|
277
|
+
except Exception: # noqa: BLE001 - best-effort hardening
|
|
278
|
+
pass
|
|
279
|
+
# Persistent storage. Automated Chromium answers navigator.storage.persist() and a
|
|
280
|
+
# persisted Storage Bucket with "denied", and WhatsApp Web (since 2026-10) halts on
|
|
281
|
+
# its loading splash with "[storage] storage bucket persistence denied
|
|
282
|
+
# (aquire-persistent-storage-denied)". Grant the real permission over CDP, and make
|
|
283
|
+
# the page-side checks agree for engines where the grant is not honoured.
|
|
284
|
+
try:
|
|
285
|
+
cdp = self._context.new_cdp_session(self._page)
|
|
286
|
+
cdp.send("Browser.grantPermissions", {"permissions": ["durableStorage"]})
|
|
287
|
+
except Exception: # noqa: BLE001 - best-effort hardening
|
|
288
|
+
pass
|
|
289
|
+
try:
|
|
290
|
+
self._context.add_init_script(_PERSISTENT_STORAGE_JS)
|
|
291
|
+
except Exception: # noqa: BLE001 - best-effort hardening
|
|
292
|
+
pass
|
|
293
|
+
try:
|
|
294
|
+
ua = self._page.evaluate("navigator.userAgent")
|
|
295
|
+
if ua and "Headless" in ua:
|
|
296
|
+
clean = ua.replace("HeadlessChrome", "Chrome")
|
|
297
|
+
client = self._context.new_cdp_session(self._page)
|
|
298
|
+
client.send("Network.setUserAgentOverride", {"userAgent": clean})
|
|
299
|
+
except Exception: # noqa: BLE001 - best-effort hardening
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
def __enter__(self) -> "PlaywrightDriver":
|
|
303
|
+
return self.start()
|
|
304
|
+
|
|
305
|
+
def __exit__(self, *_exc) -> None:
|
|
306
|
+
self.close()
|
|
307
|
+
|
|
308
|
+
def close(self) -> None:
|
|
309
|
+
for obj, meth in (
|
|
310
|
+
(self._context, "close"),
|
|
311
|
+
(self._browser, "close"),
|
|
312
|
+
(self._pw, "stop"),
|
|
313
|
+
):
|
|
314
|
+
try:
|
|
315
|
+
if obj is not None:
|
|
316
|
+
getattr(obj, meth)()
|
|
317
|
+
except Exception: # noqa: BLE001 - best-effort teardown
|
|
318
|
+
pass
|
|
319
|
+
self._page = self._context = self._browser = self._pw = None
|
|
320
|
+
|
|
321
|
+
# --- helpers ----------------------------------------------------------
|
|
322
|
+
@property
|
|
323
|
+
def page(self):
|
|
324
|
+
if self._page is None:
|
|
325
|
+
raise BrowserError("driver not started; call start() or use as a context manager")
|
|
326
|
+
return self._page
|
|
327
|
+
|
|
328
|
+
def storage_state(self, *, indexed_db: bool = True) -> dict:
|
|
329
|
+
"""Capture the current context's cookies + localStorage (+ IndexedDB).
|
|
330
|
+
|
|
331
|
+
IndexedDB capture (Playwright >=1.51) is ON by default: sites like
|
|
332
|
+
WhatsApp Web keep their auth/session in IndexedDB, so a cookies+
|
|
333
|
+
localStorage-only snapshot does NOT restore the login. Restoration is
|
|
334
|
+
automatic — `new_context(storage_state=...)` replays IndexedDB too."""
|
|
335
|
+
if self._context is None:
|
|
336
|
+
raise BrowserError("driver not started")
|
|
337
|
+
try:
|
|
338
|
+
return self._context.storage_state(indexed_db=indexed_db)
|
|
339
|
+
except TypeError:
|
|
340
|
+
# Older Playwright without the indexed_db kwarg — degrade gracefully.
|
|
341
|
+
return self._context.storage_state()
|
|
342
|
+
|
|
343
|
+
# --- BrowserDriver surface -------------------------------------------
|
|
344
|
+
def goto(self, url: str, *, timeout: int | None = None) -> None:
|
|
345
|
+
try:
|
|
346
|
+
self.page.goto(url, timeout=timeout or self._nav_timeout)
|
|
347
|
+
except Exception as exc: # noqa: BLE001
|
|
348
|
+
raise _wrap(exc, {"url": url})
|
|
349
|
+
|
|
350
|
+
def click(self, selector: str, *, timeout: int | None = None,
|
|
351
|
+
force: bool = False) -> None:
|
|
352
|
+
"""``force`` skips the actionability wait. Some frameworks paint a
|
|
353
|
+
container over their own controls, so the hit test at the element's
|
|
354
|
+
centre returns that container and the click is refused even though a
|
|
355
|
+
person can press the control. Use it only when that is what is
|
|
356
|
+
happening: it also skips the visibility check, so a genuinely hidden
|
|
357
|
+
element will be "clicked" with no effect and no error."""
|
|
358
|
+
try:
|
|
359
|
+
self.page.click(selector, timeout=timeout or self._default_timeout,
|
|
360
|
+
force=force)
|
|
361
|
+
except Exception as exc: # noqa: BLE001
|
|
362
|
+
raise _wrap(exc, {"selector": selector, "force": force})
|
|
363
|
+
|
|
364
|
+
def hover(self, selector: str, *, timeout: int | None = None) -> None:
|
|
365
|
+
"""Move the mouse over the element - for UIs that reveal controls on
|
|
366
|
+
hover (e.g. WhatsApp Web's per-message context-menu chevron)."""
|
|
367
|
+
try:
|
|
368
|
+
self.page.hover(selector, timeout=timeout or self._default_timeout)
|
|
369
|
+
except Exception as exc: # noqa: BLE001
|
|
370
|
+
raise _wrap(exc, {"selector": selector})
|
|
371
|
+
|
|
372
|
+
def context_click(self, selector: str, *, timeout: int | None = None) -> None:
|
|
373
|
+
"""Right-click the element - for app-rendered context menus (e.g.
|
|
374
|
+
WhatsApp Web's per-message menu, which opens on right-click)."""
|
|
375
|
+
try:
|
|
376
|
+
self.page.click(selector, button="right",
|
|
377
|
+
timeout=timeout or self._default_timeout)
|
|
378
|
+
except Exception as exc: # noqa: BLE001
|
|
379
|
+
raise _wrap(exc, {"selector": selector})
|
|
380
|
+
|
|
381
|
+
def fill(self, selector: str, text: str, *, timeout: int | None = None) -> None:
|
|
382
|
+
try:
|
|
383
|
+
self.page.fill(selector, text, timeout=timeout or self._default_timeout)
|
|
384
|
+
except Exception as exc: # noqa: BLE001
|
|
385
|
+
raise _wrap(exc, {"selector": selector})
|
|
386
|
+
|
|
387
|
+
def press(self, key: str, *, selector: str | None = None) -> None:
|
|
388
|
+
try:
|
|
389
|
+
if selector:
|
|
390
|
+
self.page.press(selector, key)
|
|
391
|
+
else:
|
|
392
|
+
self.page.keyboard.press(key)
|
|
393
|
+
except Exception as exc: # noqa: BLE001
|
|
394
|
+
raise _wrap(exc, {"key": key, "selector": selector})
|
|
395
|
+
|
|
396
|
+
def type_text(self, text: str, *, selector: str | None = None,
|
|
397
|
+
timeout: int | None = None) -> None:
|
|
398
|
+
"""Type ``text`` via real keyboard events into the focused element.
|
|
399
|
+
|
|
400
|
+
Unlike ``fill`` (which sets the value of a DOM <input>/<textarea> by
|
|
401
|
+
selector), this drives ``keyboard.type`` so the keystrokes reach targets
|
|
402
|
+
that are NOT addressable in the DOM - e.g. a <canvas>-rendered grid like
|
|
403
|
+
Google Sheets, where you first jump to a cell via the Name Box and then
|
|
404
|
+
type into the now-focused canvas. If ``selector`` is given it is clicked
|
|
405
|
+
first to take focus; otherwise typing goes to whatever is focused."""
|
|
406
|
+
try:
|
|
407
|
+
if selector:
|
|
408
|
+
self.page.click(selector, timeout=timeout or self._default_timeout)
|
|
409
|
+
self.page.keyboard.type(text)
|
|
410
|
+
except Exception as exc: # noqa: BLE001
|
|
411
|
+
raise _wrap(exc, {"selector": selector})
|
|
412
|
+
|
|
413
|
+
def wait(self, ms: int) -> None:
|
|
414
|
+
"""Sleep ``ms`` milliseconds (fixed delay).
|
|
415
|
+
|
|
416
|
+
For settling after an async action with no selector to await - e.g.
|
|
417
|
+
letting a debounced autosave flush before reading back. Prefer
|
|
418
|
+
``wait_for`` on a selector when one exists; this is the blunt fallback."""
|
|
419
|
+
try:
|
|
420
|
+
self.page.wait_for_timeout(ms)
|
|
421
|
+
except Exception as exc: # noqa: BLE001
|
|
422
|
+
raise _wrap(exc, {"ms": ms})
|
|
423
|
+
|
|
424
|
+
def wait_for(self, selector: str, *, timeout: int | None = None) -> None:
|
|
425
|
+
try:
|
|
426
|
+
self.page.wait_for_selector(selector, timeout=timeout or self._default_timeout)
|
|
427
|
+
except Exception as exc: # noqa: BLE001
|
|
428
|
+
raise _wrap(exc, {"selector": selector})
|
|
429
|
+
|
|
430
|
+
# --- frame-aware helpers ---------------------------------------------
|
|
431
|
+
# Some SPA overlays (e.g. LinkedIn's message compose bubble) render their
|
|
432
|
+
# input into a CHILD frame and/or seconds after the click, so a main-frame
|
|
433
|
+
# page.evaluate snapshot misses them. These poll the main frame AND every
|
|
434
|
+
# child frame with auto-waiting locators until the selector shows up.
|
|
435
|
+
|
|
436
|
+
def find_in_frames(self, selector: str, *, timeout: int | None = None,
|
|
437
|
+
step_ms: int = 400) -> int:
|
|
438
|
+
"""Index of the first frame (0 = main) with ``selector`` VISIBLE, polling
|
|
439
|
+
until ``timeout``; -1 if none. Frame order matches ``self.page.frames``."""
|
|
440
|
+
t = timeout or self._default_timeout
|
|
441
|
+
waited = 0
|
|
442
|
+
while True:
|
|
443
|
+
for i, fr in enumerate(self.page.frames):
|
|
444
|
+
try:
|
|
445
|
+
loc = fr.locator(selector).first
|
|
446
|
+
if loc.count() > 0 and loc.is_visible():
|
|
447
|
+
return i
|
|
448
|
+
except Exception: # noqa: BLE001 - a frame may be detached mid-poll
|
|
449
|
+
continue
|
|
450
|
+
if waited >= t:
|
|
451
|
+
return -1
|
|
452
|
+
self.page.wait_for_timeout(step_ms)
|
|
453
|
+
waited += step_ms
|
|
454
|
+
|
|
455
|
+
def frame_click(self, frame_index: int, selector: str,
|
|
456
|
+
*, timeout: int | None = None) -> None:
|
|
457
|
+
t = timeout or self._default_timeout
|
|
458
|
+
loc = self.page.frames[frame_index].locator(selector).first
|
|
459
|
+
try:
|
|
460
|
+
try:
|
|
461
|
+
loc.click(timeout=t)
|
|
462
|
+
except Exception: # noqa: BLE001 - actionability check failed; scroll + force
|
|
463
|
+
try:
|
|
464
|
+
loc.scroll_into_view_if_needed(timeout=t)
|
|
465
|
+
except Exception: # noqa: BLE001
|
|
466
|
+
pass
|
|
467
|
+
loc.click(timeout=t, force=True)
|
|
468
|
+
except Exception as exc: # noqa: BLE001
|
|
469
|
+
raise _wrap(exc, {"frame": frame_index, "selector": selector})
|
|
470
|
+
|
|
471
|
+
def frame_type(self, frame_index: int, selector: str, text: str,
|
|
472
|
+
*, timeout: int | None = None) -> None:
|
|
473
|
+
"""Focus ``selector`` in a frame then type via keyboard so the keystrokes
|
|
474
|
+
reach a contenteditable that ``fill`` can't set. Uses ``focus()`` (no
|
|
475
|
+
pointer-event actionability check, which the compose box in a child frame
|
|
476
|
+
can fail), falling back to a forced click."""
|
|
477
|
+
t = timeout or self._default_timeout
|
|
478
|
+
loc = self.page.frames[frame_index].locator(selector).first
|
|
479
|
+
try:
|
|
480
|
+
# locator.evaluate runs JS on the EXACT element Playwright matched -
|
|
481
|
+
# shadow-DOM aware AND with no viewport actionability check, so it works
|
|
482
|
+
# even when the compose box renders inside a web component / off-viewport
|
|
483
|
+
# (a plain document.querySelector can't see into an open shadow root).
|
|
484
|
+
try:
|
|
485
|
+
loc.evaluate("el => { try { el.scrollIntoView({block:'center'}); } catch(e){} el.focus(); }")
|
|
486
|
+
except Exception: # noqa: BLE001 - fall back to the actionable path
|
|
487
|
+
loc.focus(timeout=t)
|
|
488
|
+
self.page.keyboard.type(text)
|
|
489
|
+
except Exception as exc: # noqa: BLE001
|
|
490
|
+
raise _wrap(exc, {"frame": frame_index, "selector": selector})
|
|
491
|
+
|
|
492
|
+
def frame_el_click(self, frame_index: int, selector: str,
|
|
493
|
+
*, timeout: int | None = None) -> bool:
|
|
494
|
+
"""Click the matched element via ``locator.evaluate`` - shadow-DOM aware and
|
|
495
|
+
with no viewport actionability check, for a Send button that renders
|
|
496
|
+
off-viewport or inside a web component. Returns True if clicked (not disabled)."""
|
|
497
|
+
try:
|
|
498
|
+
return bool(self.page.frames[frame_index].locator(selector).first.evaluate(
|
|
499
|
+
"el => { if (el.disabled) return false; el.click(); return true; }"))
|
|
500
|
+
except Exception: # noqa: BLE001
|
|
501
|
+
return False
|
|
502
|
+
|
|
503
|
+
def frame_count(self, frame_index: int, selector: str) -> int:
|
|
504
|
+
"""Count elements matching ``selector`` in a frame via a Playwright
|
|
505
|
+
locator (shadow-DOM aware). Used to detect stale/extra compose bubbles."""
|
|
506
|
+
try:
|
|
507
|
+
return int(self.page.frames[frame_index].locator(selector).count())
|
|
508
|
+
except Exception: # noqa: BLE001
|
|
509
|
+
return 0
|
|
510
|
+
|
|
511
|
+
def _scoped(self, frame_index: int, container_sel: str, has_text: str, inner_sel: str):
|
|
512
|
+
"""Locator for ``inner_sel`` inside the ``container_sel`` element that
|
|
513
|
+
CONTAINS ``has_text`` - e.g. the compose box inside the conversation bubble
|
|
514
|
+
addressed to a specific person. filter(has_text=...) is Unicode-safe and
|
|
515
|
+
shadow-DOM aware, so it targets the RIGHT box even when stale bubbles are open."""
|
|
516
|
+
fr = self.page.frames[frame_index]
|
|
517
|
+
return fr.locator(container_sel).filter(has_text=has_text).locator(inner_sel)
|
|
518
|
+
|
|
519
|
+
def frame_scoped_count(self, frame_index: int, container_sel: str, has_text: str,
|
|
520
|
+
inner_sel: str) -> int:
|
|
521
|
+
try:
|
|
522
|
+
return int(self._scoped(frame_index, container_sel, has_text, inner_sel).count())
|
|
523
|
+
except Exception: # noqa: BLE001
|
|
524
|
+
return 0
|
|
525
|
+
|
|
526
|
+
def frame_scoped_text(self, frame_index: int, container_sel: str, has_text: str,
|
|
527
|
+
inner_sel: str) -> str:
|
|
528
|
+
try:
|
|
529
|
+
return self._scoped(frame_index, container_sel, has_text, inner_sel).first.inner_text(
|
|
530
|
+
timeout=self._default_timeout) or ""
|
|
531
|
+
except Exception: # noqa: BLE001
|
|
532
|
+
return ""
|
|
533
|
+
|
|
534
|
+
def frame_scoped_type(self, frame_index: int, container_sel: str, has_text: str,
|
|
535
|
+
inner_sel: str, text: str, *, timeout: int | None = None) -> None:
|
|
536
|
+
loc = self._scoped(frame_index, container_sel, has_text, inner_sel).first
|
|
537
|
+
try:
|
|
538
|
+
try:
|
|
539
|
+
loc.evaluate("el => { try { el.scrollIntoView({block:'center'}); } catch(e){} el.focus(); }")
|
|
540
|
+
except Exception: # noqa: BLE001
|
|
541
|
+
loc.focus(timeout=timeout or self._default_timeout)
|
|
542
|
+
self.page.keyboard.type(text)
|
|
543
|
+
except Exception as exc: # noqa: BLE001
|
|
544
|
+
raise _wrap(exc, {"frame": frame_index, "container": container_sel, "has_text": has_text})
|
|
545
|
+
|
|
546
|
+
def frame_scoped_click(self, frame_index: int, container_sel: str, has_text: str,
|
|
547
|
+
inner_sel: str) -> bool:
|
|
548
|
+
try:
|
|
549
|
+
return bool(self._scoped(frame_index, container_sel, has_text, inner_sel).first.evaluate(
|
|
550
|
+
"el => { if (el.disabled) return false; el.click(); return true; }"))
|
|
551
|
+
except Exception: # noqa: BLE001
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
def frame_bubble_recipients(self, frame_index: int, container_sel: str,
|
|
555
|
+
inner_sels: list) -> list:
|
|
556
|
+
"""For EACH container matching ``container_sel``, the first non-empty
|
|
557
|
+
inner_text among ``inner_sels`` (tried in order). Returns a list aligned to
|
|
558
|
+
container index - lets the caller pick the bubble addressed to a person with
|
|
559
|
+
diacritic-insensitive matching in Python (Playwright has_text cannot)."""
|
|
560
|
+
try:
|
|
561
|
+
bubbles = self.page.frames[frame_index].locator(container_sel)
|
|
562
|
+
out = []
|
|
563
|
+
for i in range(bubbles.count()):
|
|
564
|
+
txt = ""
|
|
565
|
+
for sel in inner_sels:
|
|
566
|
+
loc = bubbles.nth(i).locator(sel)
|
|
567
|
+
if loc.count():
|
|
568
|
+
txt = loc.first.inner_text(timeout=self._default_timeout) or ""
|
|
569
|
+
if txt.strip():
|
|
570
|
+
break
|
|
571
|
+
out.append(txt)
|
|
572
|
+
return out
|
|
573
|
+
except Exception: # noqa: BLE001
|
|
574
|
+
return []
|
|
575
|
+
|
|
576
|
+
def frame_nth_text(self, frame_index: int, container_sel: str, index: int,
|
|
577
|
+
inner_sel: str) -> str:
|
|
578
|
+
try:
|
|
579
|
+
return self.page.frames[frame_index].locator(container_sel).nth(index).locator(
|
|
580
|
+
inner_sel).first.inner_text(timeout=self._default_timeout) or ""
|
|
581
|
+
except Exception: # noqa: BLE001
|
|
582
|
+
return ""
|
|
583
|
+
|
|
584
|
+
def frame_nth_type(self, frame_index: int, container_sel: str, index: int,
|
|
585
|
+
inner_sel: str, text: str, *, timeout: int | None = None) -> None:
|
|
586
|
+
loc = self.page.frames[frame_index].locator(container_sel).nth(index).locator(inner_sel).first
|
|
587
|
+
try:
|
|
588
|
+
try:
|
|
589
|
+
loc.evaluate("el => { try { el.scrollIntoView({block:'center'}); } catch(e){} el.focus(); }")
|
|
590
|
+
except Exception: # noqa: BLE001
|
|
591
|
+
loc.focus(timeout=timeout or self._default_timeout)
|
|
592
|
+
self.page.keyboard.type(text)
|
|
593
|
+
except Exception as exc: # noqa: BLE001
|
|
594
|
+
raise _wrap(exc, {"frame": frame_index, "container": container_sel, "index": index})
|
|
595
|
+
|
|
596
|
+
def frame_nth_click(self, frame_index: int, container_sel: str, index: int,
|
|
597
|
+
inner_sel: str) -> bool:
|
|
598
|
+
try:
|
|
599
|
+
return bool(self.page.frames[frame_index].locator(container_sel).nth(index).locator(
|
|
600
|
+
inner_sel).first.evaluate("el => { if (el.disabled) return false; el.click(); return true; }"))
|
|
601
|
+
except Exception: # noqa: BLE001
|
|
602
|
+
return False
|
|
603
|
+
|
|
604
|
+
def frame_text(self, frame_index: int, selector: str,
|
|
605
|
+
*, timeout: int | None = None) -> str:
|
|
606
|
+
try:
|
|
607
|
+
return self.page.frames[frame_index].locator(selector).first.inner_text(
|
|
608
|
+
timeout=timeout or self._default_timeout) or ""
|
|
609
|
+
except Exception: # noqa: BLE001 - best-effort read for the recipient gate
|
|
610
|
+
return ""
|
|
611
|
+
|
|
612
|
+
def frame_eval(self, frame_index: int, script: str, *, arg: Any = None) -> Any:
|
|
613
|
+
"""Evaluate JS inside a specific frame. Used to click a button that renders
|
|
614
|
+
off-viewport (a JS ``.click()`` ignores Playwright's viewport actionability
|
|
615
|
+
check, and a real <button> fires its handler)."""
|
|
616
|
+
try:
|
|
617
|
+
return self.page.frames[frame_index].evaluate(script, arg)
|
|
618
|
+
except Exception as exc: # noqa: BLE001
|
|
619
|
+
raise _wrap(exc, {"frame": frame_index, "script": script[:120]})
|
|
620
|
+
|
|
621
|
+
def extract_text(self, selector: str | None = None,
|
|
622
|
+
*, timeout: int | None = None) -> str:
|
|
623
|
+
try:
|
|
624
|
+
if selector:
|
|
625
|
+
el = self.page.wait_for_selector(
|
|
626
|
+
selector, timeout=timeout or self._default_timeout
|
|
627
|
+
)
|
|
628
|
+
return (el.inner_text() if el else "") or ""
|
|
629
|
+
return self.page.inner_text("body") or ""
|
|
630
|
+
except Exception as exc: # noqa: BLE001
|
|
631
|
+
raise _wrap(exc, {"selector": selector})
|
|
632
|
+
|
|
633
|
+
def extract_attr(self, selector: str, attr: str,
|
|
634
|
+
*, timeout: int | None = None) -> str | None:
|
|
635
|
+
try:
|
|
636
|
+
el = self.page.wait_for_selector(
|
|
637
|
+
selector, timeout=timeout or self._default_timeout
|
|
638
|
+
)
|
|
639
|
+
return el.get_attribute(attr) if el else None
|
|
640
|
+
except Exception as exc: # noqa: BLE001
|
|
641
|
+
raise _wrap(exc, {"selector": selector, "attr": attr})
|
|
642
|
+
|
|
643
|
+
def inner_html(self, selector: str, *, timeout: int | None = None) -> str:
|
|
644
|
+
"""Return the innerHTML of the first element matching ``selector``.
|
|
645
|
+
|
|
646
|
+
Useful for structured extraction (e.g. reading a rendered <table>'s rows
|
|
647
|
+
and cells) where the joined inner_text of ``extract_text`` loses the cell
|
|
648
|
+
boundaries. Waits for the element first.
|
|
649
|
+
"""
|
|
650
|
+
try:
|
|
651
|
+
el = self.page.wait_for_selector(
|
|
652
|
+
selector, timeout=timeout or self._default_timeout
|
|
653
|
+
)
|
|
654
|
+
return (el.inner_html() if el else "") or ""
|
|
655
|
+
except Exception as exc: # noqa: BLE001
|
|
656
|
+
raise _wrap(exc, {"selector": selector})
|
|
657
|
+
|
|
658
|
+
def eval_js(self, script: str, *, arg: Any = None) -> Any:
|
|
659
|
+
"""Evaluate a JS expression/function in the page and return its result.
|
|
660
|
+
|
|
661
|
+
``script`` may be a JS expression or an arrow/function body; ``arg`` is
|
|
662
|
+
passed through to Playwright's ``page.evaluate``. Returns the JSON-able
|
|
663
|
+
result. Escape hatch for DOM reads not covered by the typed methods.
|
|
664
|
+
"""
|
|
665
|
+
try:
|
|
666
|
+
return self.page.evaluate(script, arg)
|
|
667
|
+
except Exception as exc: # noqa: BLE001
|
|
668
|
+
raise _wrap(exc, {"script": script[:120]})
|
|
669
|
+
|
|
670
|
+
def set_input_files(self, selector: str, files: Any,
|
|
671
|
+
*, timeout: int | None = None) -> None:
|
|
672
|
+
"""Set files on an ``<input type=file>`` (for uploads / media attach).
|
|
673
|
+
|
|
674
|
+
``files`` is a path or list of paths. The input is often hidden, so we
|
|
675
|
+
set files directly rather than clicking - the standard Playwright way to
|
|
676
|
+
drive a file picker (e.g. WhatsApp Web's attach -> document/photo input)."""
|
|
677
|
+
try:
|
|
678
|
+
self.page.set_input_files(selector, files,
|
|
679
|
+
timeout=timeout or self._default_timeout)
|
|
680
|
+
except Exception as exc: # noqa: BLE001
|
|
681
|
+
raise _wrap(exc, {"selector": selector})
|
|
682
|
+
|
|
683
|
+
def set_files_via_chooser(self, trigger_selector: str, files: Any,
|
|
684
|
+
*, timeout: int | None = None) -> None:
|
|
685
|
+
"""Attach files that live behind a NATIVE file chooser: click
|
|
686
|
+
``trigger_selector`` (e.g. a WhatsApp 'Photos & videos' menu item)
|
|
687
|
+
WHILE expecting the chooser, then hand it ``files``. The only reliable
|
|
688
|
+
way to attach on sites whose real ``<input type=file>`` exists only
|
|
689
|
+
transiently for the chooser (WhatsApp Web) — set_input_files on a
|
|
690
|
+
selector can't see it."""
|
|
691
|
+
t = timeout or self._default_timeout
|
|
692
|
+
try:
|
|
693
|
+
with self.page.expect_file_chooser(timeout=t) as fc:
|
|
694
|
+
self.page.click(trigger_selector, timeout=t)
|
|
695
|
+
fc.value.set_files(files, timeout=t)
|
|
696
|
+
except Exception as exc: # noqa: BLE001
|
|
697
|
+
raise _wrap(exc, {"selector": trigger_selector})
|
|
698
|
+
|
|
699
|
+
def mouse_click(self, x: float, y: float, *, button: str = "left",
|
|
700
|
+
move_first: bool = True) -> None:
|
|
701
|
+
"""Click at absolute viewport COORDINATES (not a selector).
|
|
702
|
+
|
|
703
|
+
For a target a CSS selector can't address cleanly - e.g. right-clicking
|
|
704
|
+
a WhatsApp Web media bubble to open its context menu, where the Download
|
|
705
|
+
control lives only in that menu. ``move_first`` mimics a human pointer
|
|
706
|
+
(move, settle, then click), which some SPA menus need to register."""
|
|
707
|
+
try:
|
|
708
|
+
if move_first:
|
|
709
|
+
self.page.mouse.move(x, y)
|
|
710
|
+
self.page.wait_for_timeout(120)
|
|
711
|
+
self.page.mouse.click(x, y, button=button)
|
|
712
|
+
except Exception as exc: # noqa: BLE001
|
|
713
|
+
raise _wrap(exc, {"x": x, "y": y, "button": button})
|
|
714
|
+
|
|
715
|
+
def capture_download(self, resolve_path, trigger,
|
|
716
|
+
*, timeout: int | None = None) -> dict:
|
|
717
|
+
"""Capture a browser DOWNLOAD started by ``trigger`` and save it.
|
|
718
|
+
|
|
719
|
+
``trigger`` is a zero-arg callable that performs the click which starts
|
|
720
|
+
the download (e.g. clicking a context-menu 'Download' item); it MUST run
|
|
721
|
+
inside Playwright's ``expect_download`` scope, because the listener is
|
|
722
|
+
armed before the click and resolves when the transfer begins. Once the
|
|
723
|
+
download object exists we know its ``suggested_filename``, so
|
|
724
|
+
``resolve_path(suggested) -> final_path`` lets the caller decide the
|
|
725
|
+
destination (a directory + the WhatsApp filename, or an explicit path).
|
|
726
|
+
The parent dir is created; returns
|
|
727
|
+
``{suggested_filename, saved_path, trigger_result}``. Downloads work
|
|
728
|
+
headless too - accept_downloads defaults True on launch() and
|
|
729
|
+
launch_persistent_context()."""
|
|
730
|
+
import os as _os
|
|
731
|
+
t = timeout or self._nav_timeout
|
|
732
|
+
try:
|
|
733
|
+
with self.page.expect_download(timeout=t) as dl:
|
|
734
|
+
trigger_result = trigger()
|
|
735
|
+
download = dl.value
|
|
736
|
+
suggested = download.suggested_filename
|
|
737
|
+
final = resolve_path(suggested)
|
|
738
|
+
parent = _os.path.dirname(_os.path.abspath(final))
|
|
739
|
+
if parent:
|
|
740
|
+
_os.makedirs(parent, exist_ok=True)
|
|
741
|
+
download.save_as(final)
|
|
742
|
+
return {"suggested_filename": suggested, "saved_path": str(final),
|
|
743
|
+
"trigger_result": trigger_result}
|
|
744
|
+
except Exception as exc: # noqa: BLE001
|
|
745
|
+
raise _wrap(exc, {"download": True})
|
|
746
|
+
|
|
747
|
+
def set_viewport(self, width: int, height: int) -> None:
|
|
748
|
+
"""Resize the page viewport (e.g. 1920x1080 for video-grade captures).
|
|
749
|
+
The context default is 1280x720; this changes only the live page."""
|
|
750
|
+
try:
|
|
751
|
+
self.page.set_viewport_size({"width": int(width), "height": int(height)})
|
|
752
|
+
except Exception as exc: # noqa: BLE001
|
|
753
|
+
raise _wrap(exc, {"viewport": f"{width}x{height}"})
|
|
754
|
+
|
|
755
|
+
def screenshot(self, path: str, *, full_page: bool = True) -> str:
|
|
756
|
+
try:
|
|
757
|
+
self.page.screenshot(path=path, full_page=full_page)
|
|
758
|
+
return path
|
|
759
|
+
except Exception as exc: # noqa: BLE001
|
|
760
|
+
raise _wrap(exc, {"path": path})
|
|
761
|
+
|
|
762
|
+
def render_pdf(self, html: str, path: str, *, fmt: str = "A4",
|
|
763
|
+
width: str | None = None, height: str | None = None,
|
|
764
|
+
margin: str | None = None) -> str:
|
|
765
|
+
"""Render an HTML string to a PDF file via Chromium's print engine.
|
|
766
|
+
|
|
767
|
+
``page.pdf()`` is Chromium/headless-only (the driver's default); it lays
|
|
768
|
+
the HTML out with print CSS and writes ``path``. Used for deterministic
|
|
769
|
+
document generation (e.g. reconstructing an invoice) with no live site.
|
|
770
|
+
|
|
771
|
+
``width`` + ``height`` (CSS lengths such as ``"1000mm"``) set a custom
|
|
772
|
+
page size and take precedence over ``fmt`` - large-format print (a
|
|
773
|
+
roll-up, a poster) needs an exact sheet, not a paper name. ``margin``
|
|
774
|
+
(one CSS length, all four sides; ``"0"`` for full-bleed artwork)
|
|
775
|
+
replaces the default document margins.
|
|
776
|
+
"""
|
|
777
|
+
try:
|
|
778
|
+
self.page.set_content(html, wait_until="load")
|
|
779
|
+
# Web fonts may still be decoding after "load"; printing before they
|
|
780
|
+
# are ready silently falls back to a system font.
|
|
781
|
+
self.page.evaluate("document.fonts.ready.then(() => true)")
|
|
782
|
+
opts = {"path": path, "print_background": True}
|
|
783
|
+
if width and height:
|
|
784
|
+
opts.update(width=width, height=height)
|
|
785
|
+
else:
|
|
786
|
+
opts["format"] = fmt
|
|
787
|
+
if margin is None:
|
|
788
|
+
opts["margin"] = {"top": "12mm", "bottom": "14mm", "left": "12mm", "right": "12mm"}
|
|
789
|
+
else:
|
|
790
|
+
opts["margin"] = {"top": margin, "bottom": margin, "left": margin, "right": margin}
|
|
791
|
+
self.page.pdf(**opts)
|
|
792
|
+
return path
|
|
793
|
+
except Exception as exc: # noqa: BLE001
|
|
794
|
+
raise _wrap(exc, {"path": path})
|
|
795
|
+
|
|
796
|
+
def current_url(self) -> str:
|
|
797
|
+
return self.page.url
|
|
798
|
+
|
|
799
|
+
# --- cookies ----------------------------------------------------------
|
|
800
|
+
def get_cookies(self) -> list:
|
|
801
|
+
"""Return ALL cookies in the current context (every domain).
|
|
802
|
+
|
|
803
|
+
Crucially this includes *session-scoped* cookies (no Expires/Max-Age)
|
|
804
|
+
while the browser is alive — exactly the ones Chromium will NOT write to
|
|
805
|
+
a persistent profile on disk. ``save_session`` snapshots these so they
|
|
806
|
+
can be re-injected on the next open. Returns [] if the context is gone."""
|
|
807
|
+
if self._context is None:
|
|
808
|
+
return []
|
|
809
|
+
try:
|
|
810
|
+
return list(self._context.cookies())
|
|
811
|
+
except Exception: # noqa: BLE001 - best-effort; never break a save
|
|
812
|
+
return []
|
|
813
|
+
|
|
814
|
+
def add_cookies(self, cookies: list) -> None:
|
|
815
|
+
"""Inject cookies into the current context (before navigating).
|
|
816
|
+
|
|
817
|
+
Accepts the same dicts ``get_cookies`` returns (domain+path+value+...),
|
|
818
|
+
so a saved sidecar round-trips. Best-effort: a bad cookie list must not
|
|
819
|
+
prevent the profile from opening — we drop entries Playwright rejects."""
|
|
820
|
+
if self._context is None or not cookies:
|
|
821
|
+
return
|
|
822
|
+
try:
|
|
823
|
+
self._context.add_cookies(cookies)
|
|
824
|
+
except Exception: # noqa: BLE001
|
|
825
|
+
# Fall back to injecting valid entries one-by-one so one malformed
|
|
826
|
+
# cookie can't sink the whole (still-authenticating) batch.
|
|
827
|
+
for c in cookies:
|
|
828
|
+
try:
|
|
829
|
+
self._context.add_cookies([c])
|
|
830
|
+
except Exception: # noqa: BLE001
|
|
831
|
+
pass
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _wrap(exc: Exception, details: dict | None = None) -> BrowserError:
|
|
835
|
+
"""Normalize any Playwright exception into a BrowserError with details."""
|
|
836
|
+
if isinstance(exc, BrowserError):
|
|
837
|
+
return exc
|
|
838
|
+
msg = str(exc).strip().splitlines()[0] if str(exc).strip() else exc.__class__.__name__
|
|
839
|
+
return BrowserError(msg, details or {})
|