From 9c71a91f3b2e3676f0278966d0997b28262b9780 Mon Sep 17 00:00:00 2001 From: Ishankoradia Date: Fri, 5 Jun 2026 12:10:43 +0530 Subject: [PATCH 1/2] documentation workflow updated --- .claude/commands/product/generate-docs.md | 151 --------- .claude/skills/docs-generation/SKILL.md | 243 ------------- .claude/skills/docs-generation/style-guide.md | 286 ---------------- .claude/skills/documentation/SKILL.md | 46 +++ .../skills/documentation/repo-structure.md | 64 ++++ .claude/skills/documentation/sidebar.md | 87 +++++ .../skills/documentation/style-admonitions.md | 53 +++ .claude/skills/documentation/style-images.md | 39 +++ .claude/skills/documentation/style-page.md | 99 ++++++ .claude/skills/documentation/style-writing.md | 62 ++++ .claude/skills/documentation/workflow.md | 97 ++++++ .gitignore | 8 + README.md | 67 ++-- docs/domain-map.md | 11 +- pyproject.toml | 10 + scripts/recipes/dashboards.yaml | 25 ++ scripts/recipes/ingest.yaml | 42 +++ scripts/recipes/kpis.yaml | 38 +++ scripts/recipes/metrics.yaml | 34 ++ scripts/recipes/orchestrate.yaml | 9 + scripts/recipes/pipeline_overview.yaml | 24 ++ scripts/recipes/reports.yaml | 53 +++ scripts/recipes/usage_dashboard.yaml | 12 + scripts/recipes/user_management.yaml | 22 ++ scripts/screenshot.py | 318 +++++++++++------- scripts/screenshot_docs_all.py | 293 ---------------- uv.lock | 219 ++++++++++++ 27 files changed, 1283 insertions(+), 1129 deletions(-) delete mode 100644 .claude/commands/product/generate-docs.md delete mode 100644 .claude/skills/docs-generation/SKILL.md delete mode 100644 .claude/skills/docs-generation/style-guide.md create mode 100644 .claude/skills/documentation/SKILL.md create mode 100644 .claude/skills/documentation/repo-structure.md create mode 100644 .claude/skills/documentation/sidebar.md create mode 100644 .claude/skills/documentation/style-admonitions.md create mode 100644 .claude/skills/documentation/style-images.md create mode 100644 .claude/skills/documentation/style-page.md create mode 100644 .claude/skills/documentation/style-writing.md create mode 100644 .claude/skills/documentation/workflow.md create mode 100644 pyproject.toml create mode 100644 scripts/recipes/dashboards.yaml create mode 100644 scripts/recipes/ingest.yaml create mode 100644 scripts/recipes/kpis.yaml create mode 100644 scripts/recipes/metrics.yaml create mode 100644 scripts/recipes/orchestrate.yaml create mode 100644 scripts/recipes/pipeline_overview.yaml create mode 100644 scripts/recipes/reports.yaml create mode 100644 scripts/recipes/usage_dashboard.yaml create mode 100644 scripts/recipes/user_management.yaml delete mode 100644 scripts/screenshot_docs_all.py create mode 100644 uv.lock diff --git a/.claude/commands/product/generate-docs.md b/.claude/commands/product/generate-docs.md deleted file mode 100644 index 178dbfd..0000000 --- a/.claude/commands/product/generate-docs.md +++ /dev/null @@ -1,151 +0,0 @@ -# Generate Documentation - -## Input: $ARGUMENTS - -Generate or update Docusaurus documentation for a feature, from research through screenshots to published markdown. - -## What the PM runs - -``` -/product/generate-docs "orchestrate" -/product/generate-docs "#142" -/product/generate-docs "abc123..def456" -``` - -## What it produces - -- A new or updated markdown page in `dalgo_docs/docs/` -- Screenshots in `dalgo_docs/static/img/{feature}/` -- Updated `dalgo_docs/sidebars.js` (if new page) - ---- - -## Process - -### 1. Parse Input & Determine Mode - -**Mode A — Feature Name** (default): -- `$ARGUMENTS` is a feature description (e.g. `"orchestrate"`, `"data quality"`, `"user management"`) -- Used to look up the feature in the docs-generation skill's feature-to-route table - -**Mode B — PR / Commits**: -- `$ARGUMENTS` contains a PR number (`#142`), a GitHub URL (`https://github.com/.../pull/142`), or a commit range (`abc123..def456`) -- Used to identify what changed and which feature area is affected - -To determine mode: if `$ARGUMENTS` matches `#\d+`, a GitHub PR URL, or a `\w+\.\.\w+` commit range pattern, use Mode B. Otherwise, Mode A. - -### 2. Load the Docs-Generation Skill - -Read the following files for conventions and reference: -- `.claude/skills/docs-generation/SKILL.md` — feature-to-route mapping, doc repo structure, sidebar categories -- `.claude/skills/docs-generation/style-guide.md` — writing conventions, formatting rules, anti-patterns - -### 3. Research the Feature - -**Mode A (Feature Name):** -1. Map the feature to its webapp route using SKILL.md's feature-to-route table. -2. Read the frontend page components in `webapp_v2/app/{route}/` to understand the UI. -3. Read related backend endpoints in `DDP_backend/` if the feature has API interactions. -4. Check existing docs in `dalgo_docs/docs/` — is there already a page for this feature? -5. Check existing images in `dalgo_docs/static/img/` — what screenshots already exist? - -**Mode B (PR / Commits):** -1. Run `gh pr diff $PR_NUMBER` or `git diff $COMMIT_RANGE` to see changes. -2. Identify which feature area the changes affect by looking at changed file paths. -3. Read the changed files in full context to understand the new behavior. -4. Check existing docs — does the affected feature already have documentation? -5. Determine whether this requires a new doc page or an update to existing docs. - -### 4. Determine Doc Placement - -Based on research, decide: -- **Directory**: which folder under `dalgo_docs/docs/` (e.g. `docs/`, `docs/ingest/`, `docs/managing-data/`) -- **File**: new page or update to existing page -- **Sidebar position**: where it fits in the current sidebar structure -- **Image directory**: corresponding folder under `dalgo_docs/static/img/` - -Print the placement plan: - -``` -Placement plan: -- Doc: dalgo_docs/docs/{path}/{filename}.md -- Images: dalgo_docs/static/img/{feature}/ -- Sidebar: {category or top-level position} -- Action: {new page | update existing} -``` - -**Ask the user to confirm** before proceeding. - -### 5. Capture Screenshots - -Ask the user to choose one of these options: - -1. **Capture with Playwright** — the app must be running on `localhost:3001`. Claude determines which URLs and filenames are needed from the research step, then runs: - ```bash - cd dalgo-core && python3 scripts/screenshot.py \ - --urls "/route1" "/route2" \ - --output dalgo_docs/static/img/{feature}/ \ - --names "feature_description1" "feature_description2" - ``` -2. **Insert placeholders** — add `` comments where images should go. The user captures them manually later. -3. **Skip screenshots** — write text-only documentation. - -### 6. Write Documentation - -Generate the markdown page following the style guide conventions: - -- YAML frontmatter with `sidebar_position` -- H1 title + bold one-liner summary -- Step-by-step instructions with numbered lists -- Bold UI element names (buttons, tabs, labels) -- Screenshots between steps using markdown syntax: `![Alt text](/img/{feature}/{name}.png)` -- Admonitions where appropriate (`:::info`, `:::note`, `:::warning`) -- No import+JSX for images, no external GitHub URLs for images, no jargon - -Save to: `dalgo_docs/docs/{path}/{filename}.md` - -### 7. Update Sidebar - -If this is a new page: -1. Read `dalgo_docs/sidebars.js` -2. Add the new doc ID in the correct position within the existing sidebar structure -3. Follow the patterns in SKILL.md for adding to existing categories, new top-level pages, or new categories - -If this is an update to an existing page, no sidebar changes are needed. - -### 8. Validate - -Check that: -- [ ] All image paths referenced in the markdown exist on disk (or are placeholders) -- [ ] The frontmatter `sidebar_position` doesn't conflict with existing pages -- [ ] The doc ID matches what's in `sidebars.js` -- [ ] No import+JSX image patterns were used -- [ ] No external GitHub URLs were used for images - -### 9. Print Next Steps - -``` -Documentation generated: -- Doc: dalgo_docs/docs/{path}/{filename}.md -- Images: dalgo_docs/static/img/{feature}/ ({N} screenshots) -- Sidebar: updated dalgo_docs/sidebars.js - -Next steps: -1. Preview: cd dalgo_docs && npm start -2. Review the generated page in browser at http://localhost:3000/docs/{slug} -{3. Replace screenshot placeholders (if any):} -{ } -4. Commit when satisfied -``` - ---- - -## Guidelines - -- **Write for NGO staff.** Plain language, no jargon. If a technical term is unavoidable, explain it inline. -- **Show, don't tell.** Screenshots between every major step. The user should be able to follow along visually. -- **Match existing docs.** Read 2-3 existing doc pages before writing to match the established tone and depth. -- **One feature per page.** Don't combine unrelated features. Each page should answer "how do I do X?" -- **Bold UI elements.** Every button, tab, field name, and menu item the user needs to click should be **bold**. -- **Keep it short.** If a doc page is longer than ~50 lines of content, consider splitting into sub-pages with a category index. -- **Use admonitions sparingly.** Only for genuinely important callouts — prerequisite info, automatic behaviors, or warnings about destructive actions. diff --git a/.claude/skills/docs-generation/SKILL.md b/.claude/skills/docs-generation/SKILL.md deleted file mode 100644 index 3e5b596..0000000 --- a/.claude/skills/docs-generation/SKILL.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -name: docs-generation -description: Generate and maintain Docusaurus documentation for Dalgo features. Use when creating new doc pages, updating existing docs, or reviewing documentation for completeness. Provides feature-to-route mapping, doc repo structure, sidebar conventions, and writing standards. ---- - -# Docs Generation Skill - -Reference for generating Dalgo's user-facing documentation. Maps features to webapp routes and doc locations, defines the doc repo structure, sidebar conventions, and the IA principles that govern where content lives. - -## IA Principles (read before writing anything) - -The sidebar mirrors the product left-nav exactly. Sections 1–3 are docs-only orientation. Sections 4–9 match the product navigation order. Section 10 is support convention. - -**Rule:** If a section exists in the product nav, it exists in the docs sidebar at the same level and with the same label. Don't invent groupings that don't exist in the product. - -### Two entry points, one set of reference pages - -- **Quickstart** (`quickstart/`) — short linear path for first-time users. Each page is one screen. Ends with "→ Next" links. Links *into* reference pages rather than duplicating them. -- **Reference** (all other sections) — feature docs. Trained users jump straight here. No assumed linear reading order. - -### Three user personas - -1. **Trained Dalgo user** (primary) — day-to-day user who wants reference docs they can jump into. Assumes pipelines are already set up. -2. **First-time independent user** (secondary) — needs the Quickstart linear path. -3. **Implementation partner** (tertiary) — uses the same producer-track reference but benefits from `:::note For implementation partners` callouts on pages like Warehouse setup, Transform repo switching, and User Management. - -## Feature-to-Route Mapping - -| Feature | Webapp Route(s) | Doc Location | Image Directory | -|---|---|---|---| -| Welcome / orientation | — | `docs/welcome.md` | `static/img/` | -| Quickstart | — | `docs/quickstart/` | `static/img/` | -| Glossary | — | `docs/concepts/glossary.md` | — | -| Impact / home screen | `/impact` | `docs/impact/index.md` | `static/img/impact/` | -| Charts (list) | `/charts` | `docs/charts/index.md` | `static/img/analysis/` | -| Charts (create/edit) | `/charts/new`, `/charts/[id]` | `docs/charts/creating-a-chart.md` | `static/img/analysis/` | -| Chart types | `/charts/new` (type selector) | `docs/charts/chart-types.md` | `static/img/analysis/` | -| Dashboards (list/create) | `/dashboards` | `docs/dashboards/index.md` | `static/img/analysis/` | -| Superset Usage | `/dashboards/usage` | `docs/dashboards/superset-usage.md` | `static/img/managedata/` | -| Superset | (external/embedded) | `docs/dashboards/superset.md` | `static/img/dashboards/` | -| Reports (overview/list) | `/reports` | `docs/reports/index.md` | `static/img/reports/` | -| Reports (create/view) | `/reports`, `/reports/[id]` | `docs/reports/creating.md` | `static/img/reports/` | -| Reports (comments/summary) | `/reports/[id]` | `docs/reports/comments.md` | `static/img/reports/` | -| Reports (sharing) | `/reports/[id]` | `docs/reports/sharing.md` | `static/img/reports/` | -| Reports (export/delete) | `/reports/[id]` | `docs/reports/exporting.md` | `static/img/reports/` | -| Data (section overview) | `/data` | `docs/data/index.md` | — | -| Pipeline Overview | `/data` (Overview tab) | `docs/data/overview.md` | `static/img/managedata/` | -| Ingest (overview) | `/ingest` | `docs/data/ingest/index.md` | `static/img/ingest/` | -| Connections | `/ingest` (connections tab) | `docs/data/ingest/connections.md` | `static/img/ingest/` | -| Sources | `/ingest` (sources tab) | `docs/data/ingest/sources.md` | `static/img/ingest/` | -| Warehouse | `/ingest` (warehouse tab) | `docs/data/ingest/warehouse.md` | `static/img/ingest/` | -| Transform (overview) | `/transform` | `docs/data/transform/index.md` | `static/img/transform/` | -| UI Transform | `/transform` (UI tab) | `docs/data/transform/ui-transform.md` | `static/img/transform/` | -| DBT Transform | `/transform` (DBT tab) | `docs/data/transform/dbt-transform.md` | `static/img/transform/` | -| Switching repositories | `/transform` (edit repo) | `docs/data/transform/switching-repositories.md` | `static/img/transform/` | -| Orchestrate | `/orchestrate` | `docs/data/orchestrate.md` | `static/img/orchestrate/` | -| Explore | `/explore` | `docs/data/explore.md` | `static/img/data/` | -| Data Quality | `/data-quality` | `docs/data/quality.md` | `static/img/managedata/` | -| Settings (overview) | `/settings` | `docs/settings/index.md` | — | -| User Management | `/settings/user-management` | `docs/settings/user-management.md` | `static/img/managedata/` | -| Billing | `/settings/billing` | `docs/settings/billing.md` | `static/img/settings/` | -| About | `/settings/about` | `docs/settings/about.md` | `static/img/settings/` | -| Support | — | `docs/support/index.md` | — | - -## Sidebar Structure - -``` -tutorialSidebar: - 1. welcome ← docs/welcome.md - 2. Quickstart (category → quickstart/index) - account-setup - impact - first-dashboard - first-report - next-steps - 3. Concepts (category) - concepts/glossary - 4. Impact (category → impact/index) ← no child items - 5. Charts (category → charts/index) - charts/creating-a-chart - charts/chart-types - 6. Dashboards (category → dashboards/index) - dashboards/superset-usage - dashboards/superset - 7. Reports (category → reports/index) - reports/creating - reports/comments - reports/sharing - reports/exporting - 8. Data (category → data/index) - data/overview - Ingest (nested category → data/ingest/index) - data/ingest/connections - data/ingest/sources - data/ingest/warehouse - Transform (nested category → data/transform/index) - data/transform/ui-transform - data/transform/dbt-transform - data/transform/switching-repositories - data/orchestrate - data/explore - data/quality - 9. Settings (category → settings/index) - settings/user-management - settings/billing - settings/about - 10. Support (category → support/index) - support/getting-help - support/troubleshooting -``` - -## Doc Repo Structure - -``` -dalgo_docs/ - docs/ - welcome.md # Orientation + platform overview - quickstart/ - index.md - account-setup.md - impact.md - first-dashboard.md - first-report.md - next-steps.md - concepts/ - glossary.md - impact/ - index.md - charts/ - index.md - creating-a-chart.md - chart-types.md - dashboards/ - index.md - superset-usage.md - superset.md - reports/ - index.md - creating.md - comments.md - sharing.md - exporting.md - data/ - index.md - overview.md - explore.md - orchestrate.md - quality.md - ingest/ - index.md - connections.md - sources.md - warehouse.md - transform/ - index.md - ui-transform.md - dbt-transform.md - switching-repositories.md - settings/ - index.md - user-management.md - billing.md - about.md - support/ - index.md - getting-help.md - troubleshooting.md - static/ - img/ - analysis/ # Charts and dashboards screenshots - orchestrate/ - transform/ - managedata/ # data-quality, pipeline-overview, user-management screenshots - reports/ - ingest/ - settings/ - welcome-email.png - sidebars.js - docusaurus.config.js - src/ - css/custom.css - pages/index.tsx -``` - -## Image Reference Pattern - -Use standard markdown image syntax only. Do **not** use import+JSX. - -```markdown -![Pipeline list](/img/orchestrate/pipeline_list.png) -``` - -Image paths start with `/img/` (Docusaurus serves `static/` at the site root). - -## Adding to the Sidebar - -See `dalgo_docs/sidebars.js`. Three patterns: - -**New top-level page** (rarely needed — most content lives in a section): -```javascript -tutorialSidebar: [ - 'welcome', - // ... - 'new-top-level-page', -] -``` - -**New page in existing category:** -```javascript -{ - type: 'category', - label: 'Reports', - link: { type: 'doc', id: 'reports/index' }, - items: [ - 'reports/creating', - 'reports/new-page', // add here - ], -} -``` - -**New category:** -```javascript -{ - type: 'category', - label: 'New Section', - link: { type: 'doc', id: 'new-section/index' }, - items: [ - 'new-section/page-one', - 'new-section/page-two', - ], -} -``` - -## Screenshot Policy - -- **No `` HTML comment placeholders** shipped to main. If a real screenshot is not available, use a `:::info Screenshot coming soon` admonition instead. It's honest and renders correctly. -- Screenshots live in `static/img/{feature}/`. File naming: `{feature}_{description}.png`, lowercase, underscores. -- Capture with the Playwright script at `scripts/screenshot_docs_all.py` using the staging environment. - -## Related Files - -- `style-guide.md` in this directory — writing conventions, page structure, voice, admonition rules -- `scripts/screenshot_docs_all.py` — Playwright screenshot capture for all doc pages diff --git a/.claude/skills/docs-generation/style-guide.md b/.claude/skills/docs-generation/style-guide.md deleted file mode 100644 index c78b066..0000000 --- a/.claude/skills/docs-generation/style-guide.md +++ /dev/null @@ -1,286 +0,0 @@ -# Dalgo Documentation Style Guide - -Writing conventions for Dalgo's user-facing documentation. Read at least two existing pages before writing a new one — match their tone and density, don't exceed them in length. - ---- - -## Audience - -Two personas, one voice: - -- **Trained Dalgo user** — NGO program manager or M&E officer. Knows the platform, needs quick reference. Doesn't want theory. -- **First-time user** — same profile, but starting from scratch. Guided through the Quickstart. Needs plain language and reassurance. - -**Plain language rules:** -- Write at a high-school reading level. -- Explain what things do, not how they work internally. -- If a technical term is unavoidable (e.g. "warehouse", "pipeline", "dbt"), explain it in context the first time. Or link to the glossary. -- Use "you" and "your". Address the reader directly. -- One idea per sentence. Short sentences. - ---- - -## Page structure - -Every page follows this structure: - -```markdown ---- -sidebar_position: {number} ---- - -# {Feature Name} - -**{One-sentence summary of what this feature does or lets you do.}** - -{1–2 paragraph introduction if needed. What is this? When would you use it?} - -## {First Section} - -{Step-by-step instructions or explanation.} - -## {Second Section} - -{Continue as needed.} - ---- - -**Next:** [Adjacent page](../path/page.md) · [Related page](../path/page.md) -``` - -### H1 titles are bare nouns matching the product nav label - -Use the product's label as the H1 — not a gerund, not a sentence. - -| Product label | H1 | Not this | -|---|---|---| -| Charts | `# Charts` | `# Creating Charts` | -| Warehouse | `# Warehouse` | `# Setting up your Warehouse` | -| Orchestrate | `# Orchestrate` | `# Orchestrating your Pipeline` | - -Task-focused headings (`## Creating a chart`, `## Editing a connection`) live at H2 level inside the page. - -### Bold one-liner after H1 - -Every page's first line after the H1 is a **bold one-sentence promise**: what this page covers or what the feature does. - -Good: -```markdown -# Orchestrate - -**Orchestrate lets you schedule your data pipeline to run automatically — combining sync connections and transformation tasks into a single job.** -``` - -Bad: -```markdown -# Orchestrate - -The orchestration module provides pipeline scheduling capabilities with cron-based execution. -``` - -### Every page ends with a "Next" line - -Two or three cross-references to logically adjacent pages. This stitches pages into a system. - -```markdown ---- - -**Next:** [Transform](../transform/index.md) · [Overview](../overview.md) -``` - -Or for category index pages: -```markdown ---- - -**Related:** [Dashboards](../dashboards/index.md) · [Reports](../reports/index.md) -``` - ---- - -## Instructions format - -Use numbered steps for sequential actions. Each step = one action. - -### Rules - -1. **Bold every UI element** the user needs to interact with: button labels, tab names, field labels, menu items, icon names. -2. Place a screenshot after the step it illustrates, not before. -3. Keep steps atomic — one click or one fill per step. -4. Use "Select" not "Click" (works for touchscreens too). -5. Use quotes for exact text the user should type, bold for UI labels they click. -6. **Never break a numbered list with an admonition block.** Docusaurus resets the counter after any block-level element, so a step numbered "5." after a `:::info` block renders as "1.". Either place the admonition after the last step, or fold the note inline within the step text. - -### Example - -```markdown -1. Select **Data** in the left menu, then select **Ingest**. - -![Connections list](/img/ingest/connections_list.png) - -2. Select **+ Add Connection**. -3. Give your connection a name and select the source you want to sync. -4. Select **Connect** to save. -``` - ---- - -## Voice - -| Don't | Do instead | -|---|---| -| "Click the button" | "Select **Create Pipeline**" | -| "The user should navigate to" | "Select **Reports** in the left menu" | -| "It is possible to" | "You can" | -| "In order to" | "To" | -| Passive voice | Active, second-person, present tense | - ---- - -## Admonitions - -Use sparingly. Only when the content genuinely needs to stand out. - -### `:::info` — Automatic behaviour or "good to know" - -Use when the system does something the user should know about but doesn't act on. Also use for "Screenshot coming soon" when a screenshot isn't available yet. - -```markdown -:::info -Dalgo creates a draft dashboard the moment you select **+ Create Dashboard**. If you leave without saving, the draft is kept. -::: - -:::info Screenshot coming soon -A screenshot of the warehouse connection test will be added here. -::: -``` - -### `:::note` — Helpful context or prerequisites - -Use for supplementary information that adds useful context but isn't critical to completing the task. Also use for conditional feature availability. - -```markdown -:::note -Superset is only available to organisations on the **Dalgo + Superset** plan. -::: - -:::note -You may need to whitelist these IP addresses in your firewall: `13.202.128.47`, `65.2.173.97` -::: -``` - -### `:::warning` — Destructive or irreversible actions - -Use when an action could cause data loss or is difficult to undo *once confirmed*. Dalgo often shows a confirmation dialog before a destructive action — the `:::warning` belongs near the action, not on the confirmation step itself. Do not write "This cannot be undone without canceling" — that is confusing. Write what happens after the user confirms. - -```markdown -:::warning -Deleting a connection is permanent and removes all its sync history. This cannot be undone. -::: -``` - -**Do not use** `:::tip`, `:::danger`, or `:::caution`. Stick to these three. - -### Declaring conditional features - -When a feature requires a specific subscription or setup, declare it at the top of the page with `:::note`: - -```markdown -:::note -The Superset Usage dashboard requires a **Dalgo + Superset** subscription. Contact support@dalgo.org to add it to your subscription. -::: -``` - ---- - -## Quickstart pages - -Quickstart pages are different from reference pages. They: -- Cover one screen each — short enough to read in 60 seconds -- End with "→ Next: [next step]" and "→ Reference: [reference page]" links -- Don't duplicate reference page content — they link to it -- Use a reassuring, forward-moving tone - -Template: -```markdown ---- -sidebar_position: {N} ---- - -# {Step name} - -**{What this step achieves in one sentence.}** - -## Steps - -1. ... -2. ... - -:::note -{Optional prerequisite or "if this doesn't work" note} -::: - ---- - -→ Next: [{next step}]({next-step}.md) -→ Reference: [{reference page}]({../section/page}.md) -``` - ---- - -## Images - -### Naming convention - -``` -{feature}_{description}.png -``` - -Examples: `pipeline_list.png`, `reports_create.png`, `sources_add.png` - -Lowercase, underscores, short and descriptive. - -### Directory mapping - -| Content area | Image directory | -|---|---| -| Charts / Dashboards | `static/img/analysis/` | -| Orchestrate | `static/img/orchestrate/` | -| Transform | `static/img/transform/` | -| Pipeline overview, Data Quality, User Management | `static/img/managedata/` | -| Reports | `static/img/reports/` | -| Ingest (warehouse, sources, connections) | `static/img/ingest/` | -| Settings | `static/img/settings/` | - -### Markdown reference syntax - -Always use standard markdown. Never import+JSX. - -```markdown -![Sources list](/img/ingest/sources_list.png) -``` - -### Missing screenshots - -If a real screenshot isn't available, use `:::info Screenshot coming soon` — not an HTML comment. - -```markdown -:::info Screenshot coming soon -A screenshot of the warehouse connection test will be added here. -::: -``` - ---- - -## What not to do - -| Don't | Why | -|---|---| -| `import Image from '/static/img/...'` + JSX | Docusaurus markdown renderer doesn't need it — breaks consistency | -| `` HTML comments shipped to main | Invisible in rendered docs — dishonest gap; use `:::info` instead | -| External GitHub URLs for images | Images move; use local `static/img/` | -| H1 titles with gerunds ("Creating your Dashboard") | Product labels are the H1 — gerunds live at H2 | -| Blockquotes for important notes (`> Note:`) | Use admonitions (`:::note`) — they render properly | -| Multiple unrelated features on one page | One page per feature — split if needed | -| `:::tip`, `:::danger`, `:::caution` | Not used in Dalgo docs — inconsistent rendering | -| Duplicating dbt or Superset upstream docs | Link to docs.getdbt.com or superset.apache.org instead | -| Long intro paragraphs before the first action | H1 + bold one-liner → straight into steps | diff --git a/.claude/skills/documentation/SKILL.md b/.claude/skills/documentation/SKILL.md new file mode 100644 index 0000000..e5c519d --- /dev/null +++ b/.claude/skills/documentation/SKILL.md @@ -0,0 +1,46 @@ +--- +name: documentation +description: Generate, update, or review Dalgo user-facing documentation. Use when the user asks to document a feature, write a docs page, update existing docs, or after a PR ships to refresh affected docs. +--- + +# Documentation Skill + +Reference and workflow for generating Dalgo's user-facing Docusaurus documentation. Lives in the `dalgo_docs/` repo (symlinked into `dalgo-core/`). + +## When to use + +Trigger when the user asks to: +- Generate or update a doc page for a feature ("write docs for orchestrate") +- Refresh docs after a PR or commit range ("update docs for #142") +- Review existing docs for completeness or accuracy + +Start by reading `workflow.md`. + +## IA Principles (read before writing anything) + +The sidebar mirrors the product left-nav exactly. Sections 1–3 are docs-only orientation. Sections 4–9 match the product navigation order. Section 10 is support convention. + +**Rule:** If a section exists in the product nav, it exists in the docs sidebar at the same level and with the same label. Don't invent groupings that don't exist in the product. + +### Two entry points, one set of reference pages + +- **Quickstart** (`quickstart/`) — short linear path for first-time users. Each page is one screen. Ends with "→ Next" links. Links *into* reference pages rather than duplicating them. +- **Reference** (all other sections) — feature docs. Trained users jump straight here. No assumed linear reading order. + +### Three user personas + +1. **Trained Dalgo user** (primary) — day-to-day user who wants reference docs they can jump into. Assumes pipelines are already set up. +2. **First-time independent user** (secondary) — needs the Quickstart linear path. +3. **Implementation partner** (tertiary) — uses the same producer-track reference but benefits from `:::note For implementation partners` callouts on pages like Warehouse setup, Transform repo switching, and User Management. + +## File index + +| File | Purpose | +|------|---------| +| `workflow.md` | Step-by-step process from research to published markdown | +| `sidebar.md` | Sidebar structure + patterns for adding new entries | +| `repo-structure.md` | Doc repo layout + screenshot policy | +| `style-writing.md` | Audience + voice + instructions format + anti-patterns | +| `style-page.md` | Page structure + quickstart template | +| `style-admonitions.md` | Admonition rules + conditional features | +| `style-images.md` | Image naming + directory mapping + markdown syntax | diff --git a/.claude/skills/documentation/repo-structure.md b/.claude/skills/documentation/repo-structure.md new file mode 100644 index 0000000..c6cb04e --- /dev/null +++ b/.claude/skills/documentation/repo-structure.md @@ -0,0 +1,64 @@ +# Doc Repo Structure + +Layout of the `dalgo_docs/` repo (symlinked at `dalgo-core/dalgo_docs`). + +``` +dalgo_docs/ + docs/ + welcome.md + quickstart/ + index.md + account-setup.md + impact.md + first-dashboard.md + first-report.md + next-steps.md + concepts/glossary.md + impact/index.md + charts/ + index.md + creating-a-chart.md + chart-types.md + dashboards/ + index.md + superset-usage.md + superset.md + reports/ + index.md + creating.md + comments.md + sharing.md + exporting.md + data/ + index.md + overview.md + explore.md + orchestrate.md + quality.md + ingest/{index,connections,sources,warehouse}.md + transform/{index,ui-transform,dbt-transform,switching-repositories}.md + settings/{index,user-management,billing,about}.md + support/{index,getting-help,troubleshooting}.md + static/img/{analysis,orchestrate,transform,managedata,reports,ingest,settings}/ + sidebars.js + docusaurus.config.js + src/{css/custom.css,pages/index.tsx} +``` + +## Images: placement & usage + +- **Where they live:** `static/img/{feature}/` — one folder per feature area. See `style-images.md` for the full content-area → directory mapping. +- **How they're referenced in markdown:** standard markdown syntax with paths starting `/img/` (Docusaurus serves `static/` at the site root): + ```markdown + ![Sources list](/img/ingest/sources_list.png) + ``` + Never use import + JSX. +- **Naming:** `{feature}_{description}.png`, lowercase, underscores. +- **Placement on a page:** screenshot goes *after* the step it illustrates, never before. One screenshot per major step. + +For naming, dir mapping, and missing-screenshot handling, see `style-images.md`. + +## Screenshot capture + +- Capture with `scripts/screenshot.py` (recipe-driven). Bulk = no args; per-feature = pass the recipe name. See `workflow.md` step 5. +- **No `` HTML comment placeholders** shipped to main. Use `:::info Screenshot coming soon` instead — it's honest and renders correctly. diff --git a/.claude/skills/documentation/sidebar.md b/.claude/skills/documentation/sidebar.md new file mode 100644 index 0000000..34b29d3 --- /dev/null +++ b/.claude/skills/documentation/sidebar.md @@ -0,0 +1,87 @@ +# Sidebar Structure & Patterns + +Sidebar lives in `dalgo_docs/sidebars.js`. Mirrors the product left-nav exactly. + +## Current Structure + +``` +tutorialSidebar: + 1. welcome ← docs/welcome.md + 2. Quickstart (category → quickstart/index) + account-setup + impact + first-dashboard + first-report + next-steps + 3. Concepts (category) + concepts/glossary + 4. Impact (category → impact/index) ← no child items + 5. Charts (category → charts/index) + charts/creating-a-chart + charts/chart-types + 6. Dashboards (category → dashboards/index) + dashboards/superset-usage + dashboards/superset + 7. Reports (category → reports/index) + reports/creating + reports/comments + reports/sharing + reports/exporting + 8. Data (category → data/index) + data/overview + Ingest (nested category → data/ingest/index) + data/ingest/connections + data/ingest/sources + data/ingest/warehouse + Transform (nested category → data/transform/index) + data/transform/ui-transform + data/transform/dbt-transform + data/transform/switching-repositories + data/orchestrate + data/explore + data/quality + 9. Settings (category → settings/index) + settings/user-management + settings/billing + settings/about + 10. Support (category → support/index) + support/getting-help + support/troubleshooting +``` + +## Adding to the Sidebar + +**New page in existing category:** +```javascript +{ + type: 'category', + label: 'Reports', + link: { type: 'doc', id: 'reports/index' }, + items: [ + 'reports/creating', + 'reports/new-page', // add here + ], +} +``` + +**New top-level page** (rarely needed): +```javascript +tutorialSidebar: [ + 'welcome', + // ... + 'new-top-level-page', +] +``` + +**New category:** +```javascript +{ + type: 'category', + label: 'New Section', + link: { type: 'doc', id: 'new-section/index' }, + items: [ + 'new-section/page-one', + 'new-section/page-two', + ], +} +``` diff --git a/.claude/skills/documentation/style-admonitions.md b/.claude/skills/documentation/style-admonitions.md new file mode 100644 index 0000000..800d7d4 --- /dev/null +++ b/.claude/skills/documentation/style-admonitions.md @@ -0,0 +1,53 @@ +# Style: Admonitions + +Use sparingly. Only when content genuinely needs to stand out. + +## `:::info` — Automatic behaviour or "good to know" + +System does something the user should know about but doesn't act on. Also for "Screenshot coming soon" placeholders. + +```markdown +:::info +Dalgo creates a draft dashboard the moment you select **+ Create Dashboard**. If you leave without saving, the draft is kept. +::: + +:::info Screenshot coming soon +A screenshot of the warehouse connection test will be added here. +::: +``` + +## `:::note` — Helpful context or prerequisites + +Supplementary info that adds context but isn't critical. Also for conditional feature availability. + +```markdown +:::note +Superset is only available to organisations on the **Dalgo + Superset** plan. +::: + +:::note +You may need to whitelist these IP addresses in your firewall: `13.202.128.47`, `65.2.173.97` +::: +``` + +## `:::warning` — Destructive or irreversible actions + +Use when an action could cause data loss or is hard to undo *once confirmed*. Dalgo often shows a confirmation dialog before a destructive action — the `:::warning` belongs near the action, not on the confirmation step. Do not write "This cannot be undone without canceling" — that's confusing. Write what happens after the user confirms. + +```markdown +:::warning +Deleting a connection is permanent and removes all its sync history. This cannot be undone. +::: +``` + +**Do not use** `:::tip`, `:::danger`, or `:::caution`. + +## Conditional features + +When a feature requires a specific subscription or setup, declare at the top of the page: + +```markdown +:::note +The Superset Usage dashboard requires a **Dalgo + Superset** subscription. Contact support@dalgo.org to add it. +::: +``` diff --git a/.claude/skills/documentation/style-images.md b/.claude/skills/documentation/style-images.md new file mode 100644 index 0000000..1177ec4 --- /dev/null +++ b/.claude/skills/documentation/style-images.md @@ -0,0 +1,39 @@ +# Style: Images + +## Naming convention + +``` +{feature}_{description}.png +``` + +Examples: `pipeline_list.png`, `reports_create.png`, `sources_add.png`. Lowercase, underscores, short and descriptive. + +## Directory mapping + +| Content area | Image directory | +|---|---| +| Charts / Dashboards | `static/img/analysis/` | +| Orchestrate | `static/img/orchestrate/` | +| Transform | `static/img/transform/` | +| Pipeline overview, Data Quality, User Management | `static/img/managedata/` | +| Reports | `static/img/reports/` | +| Ingest (warehouse, sources, connections) | `static/img/ingest/` | +| Settings | `static/img/settings/` | + +## Markdown reference syntax + +Always standard markdown. Never import+JSX. Paths start with `/img/` (Docusaurus serves `static/` at site root). + +```markdown +![Sources list](/img/ingest/sources_list.png) +``` + +## Missing screenshots + +If a real screenshot isn't available, use `:::info Screenshot coming soon` — not an HTML comment. + +```markdown +:::info Screenshot coming soon +A screenshot of the warehouse connection test will be added here. +::: +``` diff --git a/.claude/skills/documentation/style-page.md b/.claude/skills/documentation/style-page.md new file mode 100644 index 0000000..707484a --- /dev/null +++ b/.claude/skills/documentation/style-page.md @@ -0,0 +1,99 @@ +# Style: Page Structure + +Every reference page follows this structure: + +```markdown +--- +sidebar_position: {number} +--- + +# {Feature Name} + +**{One-sentence summary of what this feature does or lets you do.}** + +{1–2 paragraph intro if needed. What is this? When would you use it?} + +## {First Section} + +{Step-by-step instructions or explanation.} + +## {Second Section} + +{Continue as needed.} + +--- + +**Next:** [Adjacent page](../path/page.md) · [Related page](../path/page.md) +``` + +## H1 titles: bare nouns matching the product nav label + +Not a gerund, not a sentence. + +| Product label | H1 | Not this | +|---|---|---| +| Charts | `# Charts` | `# Creating Charts` | +| Warehouse | `# Warehouse` | `# Setting up your Warehouse` | +| Orchestrate | `# Orchestrate` | `# Orchestrating your Pipeline` | + +Task-focused headings (`## Creating a chart`) live at H2 inside the page. + +## Bold one-liner after H1 + +Every page's first line after H1 is a **bold one-sentence promise**. + +Good: +```markdown +# Orchestrate + +**Orchestrate lets you schedule your data pipeline to run automatically — combining sync connections and transformation tasks into a single job.** +``` + +Bad: +```markdown +# Orchestrate + +The orchestration module provides pipeline scheduling capabilities with cron-based execution. +``` + +## Every page ends with a "Next" line + +Two or three cross-references to logically adjacent pages. Use `**Related:**` instead of `**Next:**` for category index pages. + +```markdown +--- + +**Next:** [Transform](../transform/index.md) · [Overview](../overview.md) +``` + +## Quickstart pages (different from reference) + +- Cover one screen each — readable in 60 seconds +- End with "→ Next" and "→ Reference" links +- Don't duplicate reference content — link to it +- Reassuring, forward-moving tone + +Template: +```markdown +--- +sidebar_position: {N} +--- + +# {Step name} + +**{What this step achieves in one sentence.}** + +## Steps + +1. ... +2. ... + +:::note +{Optional prerequisite or "if this doesn't work" note} +::: + +--- + +→ Next: [{next step}]({next-step}.md) +→ Reference: [{reference page}]({../section/page}.md) +``` diff --git a/.claude/skills/documentation/style-writing.md b/.claude/skills/documentation/style-writing.md new file mode 100644 index 0000000..861c157 --- /dev/null +++ b/.claude/skills/documentation/style-writing.md @@ -0,0 +1,62 @@ +# Style: Writing + +Read at least two existing pages before writing a new one — match their tone and density. + +## Audience + +Two personas, one voice: +- **Trained Dalgo user** — NGO program manager or M&E officer. Knows the platform, needs quick reference. +- **First-time user** — same profile, starting from scratch. Guided through Quickstart. Needs plain language and reassurance. + +**Plain language rules:** +- High-school reading level. +- Explain what things do, not how they work internally. +- If a technical term is unavoidable ("warehouse", "pipeline", "dbt"), explain it in context the first time or link to the glossary. +- Use "you" and "your". One idea per sentence. + +## Voice + +| Don't | Do instead | +|---|---| +| "Click the button" | "Select **Create Pipeline**" | +| "The user should navigate to" | "Select **Reports** in the left menu" | +| "It is possible to" | "You can" | +| "In order to" | "To" | +| Passive voice | Active, second-person, present tense | + +## Instructions format + +Numbered steps for sequential actions. Each step = one action. + +**Rules:** +1. **Bold every UI element** the user interacts with: button labels, tab names, field labels, menu items, icon names. +2. Place a screenshot after the step it illustrates, not before. +3. Keep steps atomic — one click or one fill per step. +4. Use "Select" not "Click" (works for touchscreens too). +5. Quotes for exact text the user types; bold for UI labels they click. +6. **Never break a numbered list with an admonition block.** Docusaurus resets the counter after any block-level element — a step numbered "5." after `:::info` renders as "1.". Place admonitions after the last step, or fold the note inline. + +**Example:** +```markdown +1. Select **Data** in the left menu, then select **Ingest**. + +![Connections list](/img/ingest/connections_list.png) + +2. Select **+ Add Connection**. +3. Give your connection a name and select the source. +4. Select **Connect** to save. +``` + +## Anti-patterns + +| Don't | Why | +|---|---| +| `import Image from '/static/img/...'` + JSX | Docusaurus markdown renderer doesn't need it — breaks consistency | +| `` HTML comments on main | Invisible in rendered docs — dishonest gap; use `:::info` instead | +| External GitHub URLs for images | Images move; use local `static/img/` | +| H1 titles with gerunds ("Creating your Dashboard") | Product labels are the H1 — gerunds live at H2 | +| Blockquotes for important notes (`> Note:`) | Use admonitions (`:::note`) | +| Multiple unrelated features on one page | One page per feature — split if needed | +| `:::tip`, `:::danger`, `:::caution` | Not used in Dalgo docs | +| Duplicating dbt/Superset upstream docs | Link to docs.getdbt.com or superset.apache.org | +| Long intro paragraphs before the first action | H1 + bold one-liner → straight into steps | diff --git a/.claude/skills/documentation/workflow.md b/.claude/skills/documentation/workflow.md new file mode 100644 index 0000000..36585eb --- /dev/null +++ b/.claude/skills/documentation/workflow.md @@ -0,0 +1,97 @@ +# Documentation Workflow + +Process to generate or update a doc page, from research to published markdown. + +## 1. Parse Input & Determine Mode + +**Mode A — Feature Name** (default): input is a feature description (e.g. "orchestrate", "data quality"). + +**Mode B — PR / Commits**: input matches `#\d+`, a GitHub PR URL, or `\w+\.\.\w+`. + +## 2. Load Reference Files + +Read what's needed for the task: +- `sidebar.md`, `repo-structure.md` for placement +- `style-writing.md`, `style-page.md`, `style-admonitions.md`, `style-images.md` for conventions + +## 3. Research the Feature + +**Mode A:** +1. Find the webapp route by exploring `webapp_v2/app/` — the directory tree mirrors the product nav. +2. Read frontend page components in `webapp_v2/app/{route}/` to understand the UI. +3. Read related `DDP_backend/` endpoints if the feature has API interactions. +4. Check existing docs in `dalgo_docs/docs/` and screenshots in `dalgo_docs/static/img/`. + +**Mode B:** +1. `gh pr diff $PR_NUMBER` or `git diff $COMMIT_RANGE` to see changes. +2. Identify the affected feature area from changed file paths. +3. Read the changed files in full to understand new behavior. +4. Check existing docs for that area. + +## 4. Derive Doc Placement + +No static mapping — derive from filesystem: +- **Webapp route** → product-nav section +- **`dalgo_docs/sidebars.js`** → canonical doc folder for that section +- **Existing files under that folder** → new page vs extend existing + +**Check the domain map** at `docs/domain-map.md`. If the feature introduces a new entity or changes an existing entity's `Consumes` / `Consumed by` / `Platform-specific behaviors` / `Change impact`, the map MUST be updated. Pure UI/copy changes = no-op. + +Print the placement plan: + +``` +Placement plan: +- Doc: dalgo_docs/docs/{path}/{filename}.md +- Images: dalgo_docs/static/img/{feature}/ +- Sidebar: {category or top-level position} +- Action: {new page | update existing} +- Domain map: {no entity impact | add entity {name} | update entity {name}} +``` + +**Ask the user to confirm** before proceeding. + +## 5. Capture Screenshots + +Recipe-driven. For feature `X`, look at `scripts/recipes/X.yaml`. **Missing** → create it. **Present** → review against your current understanding (from step 3); add/update flows for any user path the docs reference but the recipe doesn't cover, and refresh selectors / waits / nuances if the UI has drifted. + +Creating or editing the recipe: +1. **Identify selectors** — read the route's `page.tsx` + imported components (and `DDP_backend` endpoints if a flow depends on server state). Prefer `data-testid` → ARIA role+name → visible text. Avoid raw CSS classes (style-fragile). +2. **Write/update the YAML** — flows × steps (navigate, click, wait, snap, press). See `kpis.yaml` / `ingest.yaml` for patterns. Required flows abort the recipe on failure; optional flows skip with a warning. Put quirks in the `nuances` field. + +Then run: `cd dalgo-core && uv run python scripts/screenshot.py X`. Verify expected files land in `dalgo_docs/static/img/{output_dir}/`. Iterate on any selector that misses — don't ship guesses. + +Use `:::info Screenshot coming soon` only when the feature isn't built. Bulk refresh (`screenshot.py` with no args) picks up every recipe automatically. + +## 6. Write Documentation + +Follow `style-writing.md` + `style-page.md`. Save to `dalgo_docs/docs/{path}/{filename}.md`. + +## 7. Update Sidebar + +New page: read `dalgo_docs/sidebars.js`, add the doc ID per `sidebar.md` patterns. Updates to existing pages need no sidebar changes. + +## 8. Update Domain Map + +If step 4 flagged it, edit `docs/domain-map.md`. Read its "Entity shape" section first; new entries must include all fields and use correct edge labels (`snapshot-of`, `compose`, `embed`, `reference`, `trigger`, `query-from`). + +**Promote to `verified` whenever possible — this is not optional.** If the feature has shipped (UI routes exist), READ the relevant Django models under `DDP_backend/ddpui/models/` plus any recent migration, cross-check against your entry, then set `Confidence: verified`. Also remove the entity from the "promote draft entries" roadmap at the bottom of the map. Use `draft` only if (a) the feature is spec-only with no shipped code, or (b) you genuinely can't locate the models — in case (b), add a note saying which paths you searched. The domain map drives `/product/write-spec` and `/engineering/plan-feature` blast-radius analysis; `draft` is contagious. + +## 9. Validate + +- [ ] Image paths exist (or are placeholders); standard markdown syntax; no GitHub URLs +- [ ] `sidebar_position` doesn't conflict; doc ID matches `sidebars.js` +- [ ] `docs/domain-map.md` updated if entity impact; Confidence = `verified` if shipped + models read + +## 10. Print Next Steps + +``` +Documentation generated: +- Doc: dalgo_docs/docs/{path}/{filename}.md +- Images: dalgo_docs/static/img/{feature}/ ({N} screenshots) +- Sidebar: updated dalgo_docs/sidebars.js + +Next: +1. Preview: cd dalgo_docs && npm start +2. Review at http://localhost:3000/docs/{slug} +3. Commit when satisfied +``` diff --git a/.gitignore b/.gitignore index b3e7c66..24dd3e7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,14 @@ # Symlinks to sibling repos (local only) DDP_backend webapp_v2 +dalgo_docs + +# secrets +.env + +# uv-managed Python env (uv.lock is committed; .venv is local) +.venv/ +__pycache__/ # mac stuff .DS_Store diff --git a/README.md b/README.md index 68f905e..5dd5bcb 100644 --- a/README.md +++ b/README.md @@ -206,32 +206,37 @@ Validates the implementation against the spec. Checks that all spec requirements /engineering/validate-spec ``` -#### `/product/generate-docs` -Generate or update a Docusaurus documentation page for a Dalgo feature. +#### Documentation (skill, no slash command) -**Mode A — Feature name:** -``` -/product/generate-docs "orchestrate" -/product/generate-docs "data quality" -``` +Triggered by natural-language prompts like: -**Mode B — PR or commit range:** ``` -/product/generate-docs "#142" -/product/generate-docs "abc123..def456" +generate docs for orchestrate +update docs for #142 +write a docs page for data quality +refresh docs for the abc123..def456 changes ``` -**Output:** Markdown page in `dalgo_docs/docs/` at the correct location per the IA, screenshots in `dalgo_docs/static/img/{feature}/`, and updated `dalgo_docs/sidebars.js` if it's a new page. +Claude auto-loads the `documentation` skill in `.claude/skills/documentation/`. The skill walks you through research → placement (derived from `webapp_v2/app/` + `dalgo_docs/sidebars.js`) → writing → sidebar update → domain-map update. + +**Output:** Markdown page in `dalgo_docs/docs/` at the correct location, screenshots in `dalgo_docs/static/img/{feature}/`, and updated `dalgo_docs/sidebars.js` if it's a new page. -The skill reads `.claude/skills/docs-generation/SKILL.md` for the feature-to-route mapping and sidebar structure, and `style-guide.md` for writing conventions. +The skill is broken into `workflow.md`, `sidebar.md`, `repo-structure.md`, and `style-*.md` files — each under 100 lines. --- -### Screenshot Script +### Screenshot Engine -Captures all documentation screenshots from the staging environment in one run. Output goes directly into `dalgo_docs/static/img/`. +Single script (`scripts/screenshot.py`) that runs YAML recipes from `scripts/recipes/`. Each recipe describes the user flows for one feature as a list of `flows` × `steps` (navigate, click, wait, snap, press). One recipe per top-level feature. + +**Setup — first time:** +```bash +cd dalgo-core +uv sync # creates .venv/, installs deps from uv.lock +uv run playwright install chromium # one-time browser download +``` -**Setup — create `dalgo_docs/.env`:** +**Create `dalgo-core/.env`** (gitignored, auto-loaded via `python-dotenv`): ``` E2E_ADMIN_EMAIL=your@email.com E2E_ADMIN_PASSWORD=yourpassword @@ -241,24 +246,18 @@ E2E_BASE_URL=https://staging-app.dalgo.org **Run:** ```bash cd dalgo-core +uv run python scripts/screenshot.py # run all recipes (bulk refresh) +uv run python scripts/screenshot.py kpis # one feature +uv run python scripts/screenshot.py kpis metrics # several +uv run python scripts/screenshot.py --list # show available recipes -# Using .env file -export $(cat ../dalgo_docs/.env | xargs) && python3 scripts/screenshot_docs_all.py - -# Or inline -E2E_ADMIN_EMAIL=your@email.com \ -E2E_ADMIN_PASSWORD=yourpassword \ -E2E_BASE_URL=https://staging-app.dalgo.org \ -python3 scripts/screenshot_docs_all.py +# Inline base URL override (skips .env's E2E_BASE_URL): +E2E_BASE_URL=http://localhost:3001 uv run python scripts/screenshot.py ``` -**What it captures:** pipeline overview, pipeline logs, user management, usage dashboard, ingest (connections, sources, warehouse form), dashboards, orchestrate, reports (list, create, detail, share, comment). +**Failure semantics:** required flows abort their recipe (exit code 2); optional flows print a warning and skip. The script logs `✓` / `⚠` / `✗` per step so you can spot drift. -**Requirements:** -```bash -pip install playwright python-dotenv -playwright install chromium -``` +**Adding a new feature:** drop a `scripts/recipes/{feature}.yaml` file. The engine auto-discovers it. See `scripts/recipes/kpis.yaml` or `ingest.yaml` for patterns. **Staging environment:** @@ -287,7 +286,7 @@ Agents are specialized personas that Claude invokes automatically when the conte |-------|-------------| | **design-review** | Combined UX expert + NGO user evaluation of UI components or screenshots. | | **tal-lens** | Tal Raviv's technology philosophy — demystify, build first, anti-hype, clarity over cleverness. | -| **docs-generation** | Feature-to-route mapping, sidebar structure, file locations, and writing conventions for Dalgo docs. Loaded automatically by `/product/generate-docs`. | +| **documentation** | Workflow + IA + style guide for Dalgo's user-facing Docusaurus docs. Triggered by prompts like "generate/update docs for X" or "write a doc page for Y". | --- @@ -327,14 +326,14 @@ Agents are specialized personas that Claude invokes automatically when the conte ### Writing or Updating Docs ```bash -# Generate a doc page for a feature -/product/generate-docs "reports" +# Generate a doc page for a feature (triggers the `documentation` skill) +"generate docs for reports" # After a PR lands, update affected docs -/product/generate-docs "#142" +"update docs for #142" # Capture all screenshots from staging in one run -export $(cat ../dalgo_docs/.env | xargs) && python3 scripts/screenshot_docs_all.py +uv run python scripts/screenshot.py # Preview the docs site locally cd ../dalgo_docs && npm start diff --git a/docs/domain-map.md b/docs/domain-map.md index 8201431..3e388fb 100644 --- a/docs/domain-map.md +++ b/docs/domain-map.md @@ -164,7 +164,7 @@ Only 1-hop edges are listed per entity. Transitive paths (Metric → Dashboard - **One Metric entity in the codebase.** The existing inline chart config shape (`column + aggregation + alias`) is `ChartMetric`. `Metric` is the persisted, reusable entity; `ChartMetric` is the inline, per-chart shape. When a chart uses a saved Metric, the resolution path is: `saved_metric_id` → `Metric` DB row → `MetricSchema` → `ChartMetric`. - Delete-blocked if consumers exist (Charts with `saved_metric_id` or KPIs with FK). - **Change impact:** Column/aggregation change flows live to every consumer on next evaluation. Renames are safe if consumers reference by ID. Deletion is blocked until consumers are removed. -- **Confidence:** `tribal-knowledge-needed` — entity doesn't exist in code yet; this entry is written from the spec and must be re-confirmed once the feature ships. +- **Confidence:** `draft` — v1 has shipped (routes `/metrics` exist with Simple + Calculated modes, blast-radius confirmation, delete-blocked behavior). Promote to `verified` after reading `models/metric.py`. ### KPI *(arriving in v1 of the Metrics & KPIs spec)* - **One-line identity:** A Metric wrapped with target + direction + RAG thresholds + trendline; leadership-facing. @@ -175,12 +175,14 @@ Only 1-hop edges are listed per entity. Transitive paths (Metric → Dashboard - ReportSnapshot (2-hop via Dashboard — KPI chart data frozen into `frozen_chart_configs` at snapshot time) - Alert (`reference` — alerts can fire on RAG transitions; deferred to Alerts spec) - **Platform-specific behaviors:** - - Target is optional. If omitted, RAG is not shown �� KPI renders as trend only. + - Target is optional. If omitted, RAG is not shown — KPI renders as trend only. - RAG thresholds are % of target, with red auto-computed. - Per-KPI time grain (team feedback) — not a page-level filter. - KPI deletion cleans up references from dashboard `components` JSON. + - **Detail drawer surfaces an Annotations timeline** — comments and beneficiary quotes added by the team, grouped by period with delta-since-last-period. This is unique to KPI (not present on Chart or Metric) and is a primary leadership-review surface. + - Linked Metric, Time Column, and Time Grain are **immutable post-create**. To change any, delete + recreate. - **Change impact:** Target change recolors historical RAG — note on backdating. Threshold change affects Alert fire rate. KPI value/target changes appear live on dashboards and live share links, but NOT in already-captured ReportSnapshots (frozen). -- **Confidence:** `tribal-knowledge-needed` — entity arriving in v1; confirm shape after ship. +- **Confidence:** `draft` — v1 has shipped (routes `/kpis` exist with two-step form, RAG threshold logic, time-grain selector, annotations drawer). Promote to `verified` after reading `models/kpi.py`. ### Dashboard - **One-line identity:** A user-composed canvas of Charts + text/heading blocks, with filters, optionally published for public viewing. @@ -352,12 +354,13 @@ Update order for the next team review session: - Explore (picker is NOT reused per Pratiksha — confirmed 2026-04-21; confirm any other MetricsSelector integrations) - Data Quality check (blocking vs non-blocking?) - Alert (paired spec shape) - - Metric / KPI (promote to `verified` after v1 ships; Metric has two paths: simple column+agg or expression, no filters, no tags) 2. Promote `draft` entries — read the actual models: - Source (`models/airbyte.py`, `ddpairbyte/`) - Warehouse (org config + adapter layer) - Transform (`models/dbt_workflow.py`, `ddpdbt/`) - Pipeline (`ddpprefect/`, `models/flow_runs.py`) + - Metric (`models/metric.py` — confirm Simple vs Calculated, validation path) + - KPI (`models/kpi.py` — confirm RAG storage, annotations table, immutability rules) - Organization, OrgUser 3. `verified` entries only need re-check on model changes: - Chart, Dashboard, ReportSnapshot, Share link (both modes), Notification diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..20a15d8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "dalgo-core-scripts" +version = "0.1.0" +description = "Utility scripts for the Dalgo docs workflow (screenshots, etc.)" +requires-python = ">=3.10" +dependencies = [ + "python-dotenv", + "playwright", + "pyyaml", +] diff --git a/scripts/recipes/dashboards.yaml b/scripts/recipes/dashboards.yaml new file mode 100644 index 0000000..04a2890 --- /dev/null +++ b/scripts/recipes/dashboards.yaml @@ -0,0 +1,25 @@ +feature: dashboards +output_dir: analysis +nuances: | + - /dashboards/create auto-creates a draft dashboard and opens the builder. + - Dashboard builder needs ~4s for the empty canvas to settle. + +flows: + - name: dashboard_list + steps: + - navigate: /dashboards + - snap: dashboard_list.png + label: Dashboards — list + + - name: chart_type_selector + steps: + - navigate: /charts/new + - snap: chart_type_selector.png + label: Charts — type selector + + - name: dashboard_builder + steps: + - navigate: /dashboards/create + - wait: 4000 + - snap: dashboard_builder.png + label: Dashboard — builder canvas diff --git a/scripts/recipes/ingest.yaml b/scripts/recipes/ingest.yaml new file mode 100644 index 0000000..eddd267 --- /dev/null +++ b/scripts/recipes/ingest.yaml @@ -0,0 +1,42 @@ +feature: ingest +output_dir: ingest +nuances: | + - Three tabs in /ingest: Connections (default), Sources, Your Warehouse. + - Tab labels are typically Title Case but may be ALL CAPS — try both. + - "+ Add Source" opens a dialog; close with Escape before tab-switching. + +flows: + - name: connections_tab + steps: + - navigate: /ingest + - snap: connections_list.png + label: Ingest — Connections tab + + - name: sources_tab + steps: + - click: + role: tab + candidates: ["Sources", "SOURCES"] + - wait: 1500 + - snap: sources_list.png + label: Ingest — Sources tab + + - name: sources_add_dialog + optional: true + steps: + - click: + role: button + candidates: ["+ Add Source", "Add Source", "ADD SOURCE"] + - wait: 1000 + - snap: sources_add.png + label: Ingest — Add Source dialog + - press: Escape + + - name: warehouse_tab + steps: + - click: + role: tab + candidates: ["Your Warehouse", "YOUR WAREHOUSE", "Warehouse"] + - wait: 1500 + - snap: warehouse_form.png + label: Ingest — Your Warehouse tab diff --git a/scripts/recipes/kpis.yaml b/scripts/recipes/kpis.yaml new file mode 100644 index 0000000..983706b --- /dev/null +++ b/scripts/recipes/kpis.yaml @@ -0,0 +1,38 @@ +feature: kpis +output_dir: kpis +nuances: | + - KPI card has data-testid="kpi-card-{id}" (dynamic per row) — use prefix match. + - Create KPI dialog is two-step; without selecting a metric on step 1 + you cannot reach step 2 — only step 1 is captured here. + - Detail drawer opens from clicking a KPI card; requires at least one KPI on staging. + +flows: + - name: list + steps: + - navigate: /kpis + - snap: kpis_list.png + label: KPIs — card grid + + - name: create_dialog_step1 + optional: true + steps: + - click: + role: button + candidates: ["+ CREATE KPI", "CREATE KPI", "Create KPI", "+ Create KPI"] + - wait: 1200 + - snap: kpis_create_step1.png + label: KPIs — create dialog (step 1) + - click: + css: "[data-slot='dialog-close']" + first: true + - wait: 600 + + - name: detail_drawer + optional: true + steps: + - click: + css: "[data-testid^='kpi-card-']" + first: true + - wait: 1500 + - snap: kpis_detail_drawer.png + label: KPIs — detail drawer diff --git a/scripts/recipes/metrics.yaml b/scripts/recipes/metrics.yaml new file mode 100644 index 0000000..db65b7f --- /dev/null +++ b/scripts/recipes/metrics.yaml @@ -0,0 +1,34 @@ +feature: metrics +output_dir: metrics +nuances: | + - "Create Metric" opens a dialog defaulting to Simple Mode. + - The Calculated Mode switcher may be a Tab or a Switch component; unverified. + - Search bar at top filters by name; not captured. + +flows: + - name: list + steps: + - navigate: /metrics + - snap: metrics_list.png + label: Metrics — list + + - name: create_simple_dialog + optional: true + steps: + - click: + role: button + candidates: ["Create Metric", "CREATE METRIC", "+ Create Metric"] + - wait: 1200 + - snap: metrics_create_simple.png + label: Metrics — Simple Mode dialog + + - name: create_calculated_dialog + optional: true + steps: + - click: + role: tab + candidates: ["Calculated", "Calculated Mode", "CALCULATED"] + - wait: 800 + - snap: metrics_create_calculated.png + label: Metrics — Calculated Mode dialog + - press: Escape diff --git a/scripts/recipes/orchestrate.yaml b/scripts/recipes/orchestrate.yaml new file mode 100644 index 0000000..e35db85 --- /dev/null +++ b/scripts/recipes/orchestrate.yaml @@ -0,0 +1,9 @@ +feature: orchestrate +output_dir: orchestrate + +flows: + - name: pipeline_list + steps: + - navigate: /orchestrate + - snap: pipeline_list.png + label: Orchestrate — pipeline list diff --git a/scripts/recipes/pipeline_overview.yaml b/scripts/recipes/pipeline_overview.yaml new file mode 100644 index 0000000..afd38a5 --- /dev/null +++ b/scripts/recipes/pipeline_overview.yaml @@ -0,0 +1,24 @@ +feature: pipeline_overview +output_dir: managedata +nuances: | + - Logs panel opens by clicking a recharts bar on the chart. + - Bar selectors vary by chart version; we try three. + - Bar click uses force=true because the SVG layer may be hover-covered. + +flows: + - name: cards + steps: + - navigate: /pipeline + - snap: pipeline_overview.png + label: Pipeline Overview — cards + + - name: expanded_logs + optional: true + steps: + - click: + css: ".recharts-bar-rectangle, [data-testid='pipeline-run-bar'], .recharts-rectangle" + first: true + force: true + - wait: 1500 + - snap: pipeline_overview_logs.png + label: Pipeline Overview — expanded logs diff --git a/scripts/recipes/reports.yaml b/scripts/recipes/reports.yaml new file mode 100644 index 0000000..2cab43d --- /dev/null +++ b/scripts/recipes/reports.yaml @@ -0,0 +1,53 @@ +feature: reports +output_dir: reports +nuances: | + - "+ CREATE REPORT" opens a dialog; Escape closes. + - Share menu opens via a per-row icon with data-testid="report-share-btn". + - Comment popover opens by clicking a comment icon on the report detail page. + - All interactive flows assume at least one report row exists on staging. + +flows: + - name: list + steps: + - navigate: /reports + - snap: reports_list.png + label: Reports — list + + - name: create_dialog + optional: true + steps: + - click: + role: button + candidates: ["+ CREATE REPORT", "CREATE REPORT", "Create Report"] + - wait: 1000 + - snap: reports_create.png + label: Reports — create dialog + - press: Escape + + - name: share_menu + optional: true + steps: + - click: + css: "[data-testid='report-share-btn']" + first: true + - wait: 800 + - snap: reports_share.png + label: Reports — share menu + - press: Escape + + - name: detail_and_comment + optional: true + steps: + - click: + css: "table tbody tr" + first: true + - wait: 2000 + - snap: reports_detail.png + label: Reports — detail view + - click: + css: "[data-testid*='comment'], [aria-label*='comment']" + first: true + - wait: 800 + - snap: reports_comment.png + label: Reports — comment popover + - press: Escape diff --git a/scripts/recipes/usage_dashboard.yaml b/scripts/recipes/usage_dashboard.yaml new file mode 100644 index 0000000..62e8b4b --- /dev/null +++ b/scripts/recipes/usage_dashboard.yaml @@ -0,0 +1,12 @@ +feature: usage_dashboard +output_dir: managedata +nuances: | + - Page embeds a Superset iframe; needs longer wait (~4s) before snapping. + +flows: + - name: dashboard + steps: + - navigate: /usage-dashboard + - wait: 4000 + - snap: usage_dashboard.png + label: Usage Dashboard diff --git a/scripts/recipes/user_management.yaml b/scripts/recipes/user_management.yaml new file mode 100644 index 0000000..691a04f --- /dev/null +++ b/scripts/recipes/user_management.yaml @@ -0,0 +1,22 @@ +feature: user_management +output_dir: managedata +nuances: | + - Invite User opens a modal dialog. Button label may be casing-variant. + +flows: + - name: users_table + steps: + - navigate: /settings/user-management + - snap: user_management.png + label: User Management — users table + + - name: invite_dialog + optional: true + steps: + - click: + role: button + candidates: ["Invite User", "INVITE USER"] + - wait: 1000 + - snap: user_management_invite.png + label: User Management — invite dialog + - press: Escape diff --git a/scripts/screenshot.py b/scripts/screenshot.py index 847daff..dcafbf6 100755 --- a/scripts/screenshot.py +++ b/scripts/screenshot.py @@ -1,20 +1,19 @@ #!/usr/bin/env python3 """ -Playwright screenshot utility for Dalgo documentation. +Dalgo docs screenshot engine. -Logs into the Dalgo webapp and captures screenshots of specified routes. -Reuses the same auth pattern as webapp_v2/e2e/login.spec.ts. +Loads YAML recipes from scripts/recipes/ and executes them against a logged-in +Playwright session. Each recipe describes the user flows for one product feature +as a list of `flows`, each containing `steps` (navigate, click, wait, snap, press). Usage: - python3 scripts/screenshot.py \ - --urls "/orchestrate" "/orchestrate/create" \ - --output dalgo_docs/static/img/orchestrate/ \ - --names "pipeline_list" "pipeline_create" - -Environment variables: - E2E_ADMIN_EMAIL Login email (required) - E2E_ADMIN_PASSWORD Login password (required) - E2E_BASE_URL Base URL (default: http://localhost:3001) + uv run python scripts/screenshot.py # run all recipes + uv run python scripts/screenshot.py kpis # one recipe + uv run python scripts/screenshot.py kpis metrics # several + uv run python scripts/screenshot.py --list # list recipes + +Environment (from dalgo-core/.env): + E2E_ADMIN_EMAIL, E2E_ADMIN_PASSWORD, E2E_BASE_URL """ import argparse @@ -22,138 +21,221 @@ import sys from pathlib import Path -from playwright.sync_api import sync_playwright +import yaml +from dotenv import load_dotenv +from playwright.sync_api import sync_playwright, Page +load_dotenv(Path(__file__).parent.parent / ".env") -def parse_args(): - parser = argparse.ArgumentParser( - description="Capture screenshots of Dalgo webapp pages" - ) - parser.add_argument( - "--urls", - nargs="+", - required=True, - help="Route paths to screenshot (e.g. /orchestrate /transform)", - ) - parser.add_argument( - "--output", - required=True, - help="Output directory for screenshots", - ) - parser.add_argument( - "--names", - nargs="+", - required=True, - help="Filenames for each screenshot (without extension), must match --urls count", - ) - parser.add_argument( - "--width", - type=int, - default=1470, - help="Viewport width (default: 1470)", - ) - parser.add_argument( - "--height", - type=int, - default=900, - help="Viewport height (default: 900)", - ) - parser.add_argument( - "--wait", - type=int, - default=3000, - help="Wait time in ms after page load (default: 3000)", - ) - parser.add_argument( - "--full-page", - action="store_true", - help="Capture full page instead of just the viewport", - ) - return parser.parse_args() - +REPO_ROOT = Path(__file__).parent.parent +RECIPES_DIR = REPO_ROOT / "scripts" / "recipes" +# Resolve dalgo_docs via the symlink at dalgo-core/dalgo_docs (set up in the repo root). +# This is the sanctioned path — don't assume a sibling layout. +DOCS_IMG_ROOT = (REPO_ROOT / "dalgo_docs" / "static" / "img").resolve() +BASE_URL = os.environ.get("E2E_BASE_URL", "https://staging-app.dalgo.org") +EMAIL = os.environ.get("E2E_ADMIN_EMAIL") +PASSWORD = os.environ.get("E2E_ADMIN_PASSWORD") -def login(page, base_url, email, password): - """Log into Dalgo using the same flow as webapp_v2/e2e/login.spec.ts.""" - page.goto(f"{base_url}/login") +RESULTS = [] # list of (feature, label, path) +FAILURES = [] # required-step failures: (feature, message) +WARNINGS = [] # optional-flow skips: (feature, flow_name, message) - # Wait for the login form to be ready - page.get_by_label("Business Email*").wait_for(state="visible", timeout=15000) - # Fill credentials - page.get_by_label("Business Email*").fill(email) - page.get_by_label("Password*").fill(password) - - # Click sign in and wait for redirect to /impact - page.get_by_role("button", name="Sign In").click() - page.wait_for_url("**/impact", timeout=15000) +def login(page: Page): + page.goto(f"{BASE_URL}/login") + page.wait_for_load_state("load") + page.wait_for_timeout(2000) + email_input = ( + page.get_by_label("Business Email*") + if page.get_by_label("Business Email*").count() > 0 + else page.get_by_placeholder("eg. user@domain.com") + ) + email_input.fill(EMAIL) -def capture_screenshots(page, base_url, urls, output_dir, names, wait_ms, full_page): - """Navigate to each URL and capture a screenshot.""" - results = [] + password_input = ( + page.get_by_label("Password*") + if page.get_by_label("Password*").count() > 0 + else page.get_by_placeholder("Enter your password") + ) + password_input.fill(PASSWORD) + + page.get_by_role("button", name="SIGN IN").click() + page.wait_for_function("window.location.pathname !== '/login'", timeout=20000) + page.wait_for_load_state("load") + page.wait_for_timeout(1500) + print(f"✓ Login successful — landed on {page.url}\n") + + +def discover_recipes(): + """Return {name: path} for all *.yaml files in RECIPES_DIR.""" + return {p.stem: p for p in sorted(RECIPES_DIR.glob("*.yaml"))} + + +def load_recipe(path: Path) -> dict: + with open(path) as f: + return yaml.safe_load(f) + + +def resolve_locator(page: Page, spec: dict): + """Resolve a click spec to a Playwright locator (or None if no element matched).""" + if "role" in spec: + role = spec["role"] + candidates = spec.get("candidates") or ([spec["name"]] if "name" in spec else []) + for name in candidates: + loc = page.get_by_role(role, name=name) + if loc.count() > 0: + return loc.first if spec.get("first") else loc + return None + if "css" in spec: + loc = page.locator(spec["css"]) + if loc.count() == 0: + return None + return loc.first if spec.get("first") else loc + raise ValueError(f"click spec needs 'role' or 'css': {spec}") + + +def execute_step(page: Page, step: dict, feature: str, output_dir: str): + """Run one step. Raises RuntimeError on failure (caller decides what to do).""" + if "navigate" in step: + page.goto(f"{BASE_URL}{step['navigate']}") + page.wait_for_load_state("load") + page.wait_for_timeout(2000) + return + + if "wait" in step: + page.wait_for_timeout(int(step["wait"])) + return + + if "click" in step: + spec = step["click"] + loc = resolve_locator(page, spec) + if loc is None: + raise RuntimeError(f"click selector not found: {spec}") + loc.click(force=bool(spec.get("force"))) + return + + if "press" in step: + page.keyboard.press(step["press"]) + return + + if "snap" in step: + filename = step["snap"] + label = step.get("label", filename) + out = DOCS_IMG_ROOT / output_dir / filename + out.parent.mkdir(parents=True, exist_ok=True) + page.wait_for_timeout(1000) + page.screenshot(path=str(out), full_page=bool(step.get("full_page"))) + RESULTS.append((feature, label, str(out))) + print(f" ✓ {label} → {out.relative_to(DOCS_IMG_ROOT)}") + return + + raise ValueError(f"unknown step keys: {list(step.keys())}") + + +def run_flow(page: Page, flow: dict, feature: str, output_dir: str): + """Run all steps in a flow. Optional flows skip silently on failure; required abort the recipe.""" + name = flow.get("name", "") + optional = bool(flow.get("optional")) + print(f" ↳ flow: {name}{' (optional)' if optional else ''}") + try: + for step in flow.get("steps", []): + execute_step(page, step, feature, output_dir) + except Exception as e: + msg = str(e) + if optional: + WARNINGS.append((feature, name, msg)) + print(f" ⚠ skipped: {msg}") + else: + raise # propagate to abort recipe + + +def run_recipe(page: Page, name: str, path: Path): + print(f"\n--- {name} ---") + recipe = load_recipe(path) + feature = recipe.get("feature", name) + output_dir = recipe.get("output_dir", feature) + nuances = (recipe.get("nuances") or "").strip() + if nuances: + for line in nuances.splitlines(): + print(f" # {line.strip()}") + + try: + for flow in recipe.get("flows", []): + run_flow(page, flow, feature, output_dir) + except RuntimeError as e: + FAILURES.append((feature, str(e))) + print(f" ✗ ABORT recipe '{name}': {e}") - for url, name in zip(urls, names): - full_url = f"{base_url}{url}" - output_path = output_dir / f"{name}.png" - print(f" Navigating to {url}...") - page.goto(full_url) - page.wait_for_load_state("networkidle") - page.wait_for_timeout(wait_ms) +def parse_args(): + parser = argparse.ArgumentParser( + description="Run YAML recipes to capture Dalgo docs screenshots." + ) + parser.add_argument( + "recipes", nargs="*", + help="Recipe names to run (e.g. 'kpis metrics'). Default: all.", + ) + parser.add_argument("--list", action="store_true", help="List available recipes and exit.") + return parser.parse_args() - page.screenshot(path=str(output_path), full_page=full_page) - print(f" Saved: {output_path}") - results.append(output_path) - return results +def main() -> int: + args = parse_args() + available = discover_recipes() + if args.list: + print(f"Available recipes (in {RECIPES_DIR.relative_to(REPO_ROOT)}):") + for n, p in available.items(): + print(f" {n}") + return 0 -def main(): - args = parse_args() + if not available: + print(f"Error: no recipes found in {RECIPES_DIR}", file=sys.stderr) + return 1 - if len(args.urls) != len(args.names): - print( - f"Error: --urls has {len(args.urls)} entries but --names has {len(args.names)}. They must match.", - file=sys.stderr, - ) - sys.exit(1) + unknown = [r for r in args.recipes if r not in available] + if unknown: + print(f"Error: unknown recipes: {unknown}", file=sys.stderr) + print(f"Available: {list(available.keys())}", file=sys.stderr) + return 1 - email = os.environ.get("E2E_ADMIN_EMAIL") - password = os.environ.get("E2E_ADMIN_PASSWORD") - base_url = os.environ.get("E2E_BASE_URL", "http://localhost:3001") + to_run = {r: available[r] for r in args.recipes} if args.recipes else available - if not email or not password: - print( - "Error: E2E_ADMIN_EMAIL and E2E_ADMIN_PASSWORD environment variables are required.", - file=sys.stderr, - ) - sys.exit(1) + if not EMAIL or not PASSWORD: + print("Error: E2E_ADMIN_EMAIL and E2E_ADMIN_PASSWORD must be set in .env.", file=sys.stderr) + return 1 - output_dir = Path(args.output) - output_dir.mkdir(parents=True, exist_ok=True) + print(f"Base URL: {BASE_URL}") + print(f"Recipes: {', '.join(to_run)}") + print(f"Output: {DOCS_IMG_ROOT}\n") with sync_playwright() as p: browser = p.chromium.launch(headless=True) - context = browser.new_context( - viewport={"width": args.width, "height": args.height} - ) + context = browser.new_context(viewport={"width": 1470, "height": 900}) page = context.new_page() - print(f"Logging in to {base_url}...") - login(page, base_url, email, password) - print("Login successful.") + print(f"Logging in as {EMAIL}...") + login(page) - print(f"Capturing {len(args.urls)} screenshot(s)...") - results = capture_screenshots( - page, base_url, args.urls, output_dir, args.names, args.wait, args.full_page - ) + for name, path in to_run.items(): + run_recipe(page, name, path) browser.close() - print(f"\nDone. {len(results)} screenshot(s) saved to {output_dir}/") - for path in results: - print(f" {path}") + print(f"\n{'='*60}") + print(f"✓ {len(RESULTS)} screenshots saved") + if WARNINGS: + print(f"⚠ {len(WARNINGS)} optional flows skipped:") + for feature, flow, msg in WARNINGS: + print(f" - {feature}/{flow}: {msg}") + if FAILURES: + print(f"✗ {len(FAILURES)} recipes aborted on required-step failure:") + for feature, msg in FAILURES: + print(f" - {feature}: {msg}") + print("="*60) + return 0 if not FAILURES else 2 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/scripts/screenshot_docs_all.py b/scripts/screenshot_docs_all.py deleted file mode 100644 index c545b25..0000000 --- a/scripts/screenshot_docs_all.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/env python3 -""" -Capture all documentation screenshots for Dalgo docs. -Handles tab switching, dialog opening, and interactive states. - -Usage: - E2E_ADMIN_EMAIL=... E2E_ADMIN_PASSWORD=... E2E_BASE_URL=https://staging-app.dalgo.org \ - python3 scripts/screenshot_docs_all.py - -Output dirs are relative to the repo root (dalgo-core/). -""" - -import os -import sys -from pathlib import Path - -from playwright.sync_api import sync_playwright, Page - -BASE_URL = os.environ.get("E2E_BASE_URL", "https://staging-app.dalgo.org") -EMAIL = os.environ.get("E2E_ADMIN_EMAIL") -PASSWORD = os.environ.get("E2E_ADMIN_PASSWORD") -DOCS_ROOT = Path(__file__).parent.parent.parent / "dalgo_docs" / "static" / "img" - -RESULTS = [] -FAILURES = [] - - -def ensure_dirs(): - for d in ["ingest", "analysis", "managedata", "orchestrate", "reports", "transform"]: - (DOCS_ROOT / d).mkdir(parents=True, exist_ok=True) - - -def login(page: Page): - page.goto(f"{BASE_URL}/login") - page.wait_for_load_state("load") - page.wait_for_timeout(2000) - - # Fill email — try by label, placeholder, or input type - email_input = ( - page.get_by_label("Business Email*") - if page.get_by_label("Business Email*").count() > 0 - else page.get_by_placeholder("eg. user@domain.com") - ) - email_input.fill(EMAIL) - - password_input = ( - page.get_by_label("Password*") - if page.get_by_label("Password*").count() > 0 - else page.get_by_placeholder("Enter your password") - ) - password_input.fill(PASSWORD) - - # Click sign in button (case-insensitive match) - page.get_by_role("button", name="SIGN IN").click() - - # Wait for redirect away from /login (to /impact or wherever) - page.wait_for_function("window.location.pathname !== '/login'", timeout=20000) - page.wait_for_load_state("load") - page.wait_for_timeout(1500) - print(f"✓ Login successful — landed on {page.url}\n") - - -def snap(page: Page, rel_path: str, label: str, full_page: bool = False): - """Take a screenshot and record result.""" - out = DOCS_ROOT / rel_path - try: - page.wait_for_timeout(1500) - page.screenshot(path=str(out), full_page=full_page) - RESULTS.append((label, str(out))) - print(f" ✓ {label} → {rel_path}") - except Exception as e: - FAILURES.append((label, str(e))) - print(f" ✗ {label} FAILED: {e}") - - -def try_click(page: Page, selector_fn, timeout=3000) -> bool: - """Try to click an element; return True if successful.""" - try: - el = selector_fn() - el.wait_for(state="visible", timeout=timeout) - el.click() - return True - except Exception: - return False - - -def pipeline_overview(page: Page): - print("--- Pipeline Overview ---") - page.goto(f"{BASE_URL}/pipeline") - page.wait_for_load_state("load") - page.wait_for_timeout(2000) - snap(page, "managedata/pipeline_overview.png", "Pipeline Overview — cards") - - # Click a bar to expand logs - clicked = False - for sel in [ - ".recharts-bar-rectangle", - "[data-testid='pipeline-run-bar']", - ".recharts-rectangle", - ]: - bars = page.locator(sel).all() - if bars: - try: - bars[0].click(force=True) - page.wait_for_timeout(1500) - snap(page, "managedata/pipeline_overview_logs.png", "Pipeline Overview — expanded logs") - clicked = True - break - except Exception: - pass - if not clicked: - print(" ⚠ Could not click a bar to expand logs") - - -def user_management(page: Page): - print("--- User Management ---") - page.goto(f"{BASE_URL}/settings/user-management") - page.wait_for_load_state("load") - page.wait_for_timeout(2000) - snap(page, "managedata/user_management.png", "User Management — users table") - - # Open Invite User dialog - for name in ["Invite User", "INVITE USER"]: - btn = page.get_by_role("button", name=name) - if btn.is_visible(): - btn.click() - page.wait_for_timeout(1000) - snap(page, "managedata/user_management_invite.png", "User Management — invite dialog") - page.keyboard.press("Escape") - break - - -def usage_dashboard(page: Page): - print("--- Usage Dashboard ---") - page.goto(f"{BASE_URL}/usage-dashboard") - page.wait_for_load_state("load") - page.wait_for_timeout(4000) # Superset iframe may take longer - snap(page, "managedata/usage_dashboard.png", "Usage Dashboard") - - -def superset(page: Page): - print("--- Superset ---") - page.goto(f"{BASE_URL}/analysis/superset") - page.wait_for_load_state("load") - page.wait_for_timeout(3000) - snap(page, "analysis/superset_signin.png", "Superset sign-in") - - -def ingest(page: Page): - print("--- Ingest ---") - # Connections (default tab) - page.goto(f"{BASE_URL}/ingest") - page.wait_for_load_state("load") - page.wait_for_timeout(2000) - snap(page, "ingest/connections_list.png", "Ingest — Connections tab") - - # Sources tab - for tab_name in ["Sources", "SOURCES"]: - tab = page.get_by_role("tab", name=tab_name) - if tab.is_visible(): - tab.click() - page.wait_for_timeout(1500) - snap(page, "ingest/sources_list.png", "Ingest — Sources tab") - - # Try to open Add Source dialog - for btn_name in ["+ Add Source", "Add Source", "ADD SOURCE"]: - btn = page.get_by_role("button", name=btn_name) - if btn.is_visible(): - btn.click() - page.wait_for_timeout(1000) - snap(page, "ingest/sources_add.png", "Ingest — Add Source dialog") - page.keyboard.press("Escape") - break - break - - # Your Warehouse tab - for tab_name in ["Your Warehouse", "YOUR WAREHOUSE", "Warehouse"]: - tab = page.get_by_role("tab", name=tab_name) - if tab.is_visible(): - tab.click() - page.wait_for_timeout(1500) - snap(page, "ingest/warehouse_form.png", "Ingest — Your Warehouse tab") - break - - -def dashboards(page: Page): - print("--- Dashboards ---") - page.goto(f"{BASE_URL}/dashboards") - page.wait_for_load_state("load") - page.wait_for_timeout(2000) - snap(page, "analysis/dashboard_list.png", "Dashboards — list") - - # Chart type selector - page.goto(f"{BASE_URL}/charts/new") - page.wait_for_load_state("load") - page.wait_for_timeout(2000) - snap(page, "analysis/chart_type_selector.png", "Charts — type selector") - - # Dashboard builder — navigate to create, which auto-creates and opens builder - page.goto(f"{BASE_URL}/dashboards/create") - page.wait_for_load_state("load") - page.wait_for_timeout(4000) - snap(page, "analysis/dashboard_builder.png", "Dashboard — builder canvas") - - -def orchestrate(page: Page): - print("--- Orchestrate ---") - page.goto(f"{BASE_URL}/orchestrate") - page.wait_for_load_state("load") - page.wait_for_timeout(2000) - snap(page, "orchestrate/pipeline_list.png", "Orchestrate — pipeline list") - - -def reports(page: Page): - print("--- Reports ---") - page.goto(f"{BASE_URL}/reports") - page.wait_for_load_state("load") - page.wait_for_timeout(2000) - snap(page, "reports/reports_list.png", "Reports — list") - - # Open Create Report dialog - for btn_name in ["CREATE REPORT", "Create Report", "+ CREATE REPORT"]: - btn = page.get_by_role("button", name=btn_name) - if btn.is_visible(): - btn.click() - page.wait_for_timeout(1000) - snap(page, "reports/reports_create.png", "Reports — create dialog") - page.keyboard.press("Escape") - break - - # Share menu — click share icon on first report row - share_btns = page.get_by_test_id("report-share-btn").all() - if share_btns: - share_btns[0].click() - page.wait_for_timeout(800) - snap(page, "reports/reports_share.png", "Reports — share menu") - page.keyboard.press("Escape") - - # Comment — open a report and capture comment popover - report_rows = page.locator("table tbody tr").all() - if report_rows: - report_rows[0].click() - page.wait_for_load_state("load") - page.wait_for_timeout(2000) - snap(page, "reports/reports_detail.png", "Reports — detail view") - - # Try clicking comment icon - comment_btns = page.locator("[data-testid*='comment'], [aria-label*='comment']").all() - if comment_btns: - comment_btns[0].click() - page.wait_for_timeout(800) - snap(page, "reports/reports_comment.png", "Reports — comment popover") - page.keyboard.press("Escape") - - -def main(): - if not EMAIL or not PASSWORD: - print("Error: E2E_ADMIN_EMAIL and E2E_ADMIN_PASSWORD must be set.", file=sys.stderr) - sys.exit(1) - - ensure_dirs() - print(f"Base URL: {BASE_URL}") - print(f"Output: {DOCS_ROOT}\n") - - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - context = browser.new_context(viewport={"width": 1470, "height": 900}) - page = context.new_page() - - print(f"Logging in as {EMAIL}...") - login(page) - - pipeline_overview(page) - user_management(page) - usage_dashboard(page) - ingest(page) - dashboards(page) - orchestrate(page) - reports(page) - - browser.close() - - print(f"\n{'='*50}") - print(f"✓ {len(RESULTS)} screenshots saved") - if FAILURES: - print(f"✗ {len(FAILURES)} failed:") - for label, err in FAILURES: - print(f" - {label}: {err}") - print("="*50) - - -if __name__ == "__main__": - main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ba9be53 --- /dev/null +++ b/uv.lock @@ -0,0 +1,219 @@ +version = 1 +revision = 2 +requires-python = ">=3.10" + +[[package]] +name = "dalgo-core-scripts" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "playwright" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "playwright" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, +] + +[[package]] +name = "greenlet" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/21/117c8710abb7f146d804a124c07eb5964a60b90d02b72452885aecc18efa/greenlet-3.5.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7eacb17a9d41538a2bc4912eba5ef13823c83cb69e4d141d0813debe7163187f", size = 283510, upload-time = "2026-05-20T13:12:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f7/6762a56fa5f6c2295c449c6524e10ce481e381c994cc44d9d03aef0700fb/greenlet-3.5.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5cc9606aa5f4e0bde0d3bd502b44f743864c3ffa5cfa1011b1e30f5aa02366f", size = 599696, upload-time = "2026-05-20T14:00:02.906Z" }, + { url = "https://files.pythonhosted.org/packages/0f/05/85a511e68ee109aff0aa00b4b497806091dd2d82ce209e49c6e801bd5d92/greenlet-3.5.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3d35f87c7253b715d13d679e0783d845910144f282cb939fe1ba4ac8616269c", size = 612618, upload-time = "2026-05-20T14:05:39.202Z" }, + { url = "https://files.pythonhosted.org/packages/2e/19/60df45065b2981ff894fdd51e7c99a3a4b107412822b083d88d5d528f663/greenlet-3.5.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:00929c98ec525fd9bf075875d8c5f6a983a90906cdf78a66e6de2d8e466c2a19", size = 619237, upload-time = "2026-05-20T14:09:06.421Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/8b83d18ae07c46c019617f35afd7b47aab7f9b4fbb12fc637d681e10bdd8/greenlet-3.5.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:540dae7b956209af4d70a3be35927b4055f617763771e5e84a5255bea934d2f5", size = 612947, upload-time = "2026-05-20T13:14:23.469Z" }, + { url = "https://files.pythonhosted.org/packages/26/9a/4ba4c2bc9d9df5f41bb8943fb7bb11e440352e6b9c2e36716b6e85f8b82d/greenlet-3.5.1-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:001775efe7b8e758861294c7a27c28af87f3f3f1c20468a2bc618c45b346c061", size = 415653, upload-time = "2026-05-20T14:01:36.999Z" }, + { url = "https://files.pythonhosted.org/packages/5d/14/ad1f9fc9b82384c010212464a3702bd911f95dab2f1180bc6fbcfb1f958c/greenlet-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed8cdb691169715a9a492844a83246f090182247d1a5031dc78a403f68ba1e97", size = 1571425, upload-time = "2026-05-20T14:02:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/46/1c/43b8203cf10f4292c9e3d270e9e5f5ade79115a0a0ca5ea6f1be5f8915a7/greenlet-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d59e840387076a51016777a9328b3f2c427c6f9208a6e958bad251be50a648d", size = 1638688, upload-time = "2026-05-20T13:14:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/ac/6e/0344b1e99f58f71715456e46492101fd2daa408957b8186ade0a4b515da7/greenlet-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:b9152fca4a6466e114aaec745ae61cba739903a109754a9d4e1262f01e9259b1", size = 237763, upload-time = "2026-05-20T13:11:35.659Z" }, + { url = "https://files.pythonhosted.org/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", size = 284764, upload-time = "2026-05-20T13:09:10.204Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", size = 603479, upload-time = "2026-05-20T14:00:04.757Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", size = 615495, upload-time = "2026-05-20T14:05:40.87Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/8fd452fd81adb9ec79c8275c1375702ab0fd6bee4952da12eaa09b9508d8/greenlet-3.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360", size = 623515, upload-time = "2026-05-20T14:09:07.853Z" }, + { url = "https://files.pythonhosted.org/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", size = 614754, upload-time = "2026-05-20T13:14:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bc/c318aa9f3ffc77320fddcee3d892be957b42e2ff947198d9450b004f3a38/greenlet-3.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747", size = 418439, upload-time = "2026-05-20T14:01:38.446Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", size = 1574097, upload-time = "2026-05-20T14:02:24.003Z" }, + { url = "https://files.pythonhosted.org/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", size = 1641058, upload-time = "2026-05-20T13:14:31.83Z" }, + { url = "https://files.pythonhosted.org/packages/f5/56/5f332b7705545eac2dc01b4e9254d24a793f2656d55d5cc6b94ee59d22ae/greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e", size = 238089, upload-time = "2026-05-20T13:14:03.229Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a9/a3c2fa886c5b94863fb0e61b3bc14610b7aa94cf4f17f8741b11708305fc/greenlet-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:cc6ab7e555c8a112ad3a76e368e86e12a2754bcae1652a5602e133ec7b635523", size = 234989, upload-time = "2026-05-20T13:08:27.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" }, + { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" }, + { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" }, + { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580, upload-time = "2026-05-20T13:08:45.056Z" }, + { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, + { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" }, + { url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767, upload-time = "2026-05-20T14:09:12.15Z" }, + { url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786, upload-time = "2026-05-20T14:01:42.064Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" }, + { url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" }, + { url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607, upload-time = "2026-05-20T14:09:13.949Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287, upload-time = "2026-05-20T14:01:43.143Z" }, + { url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" }, + { url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" }, + { url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962, upload-time = "2026-05-20T14:09:15.532Z" }, + { url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047, upload-time = "2026-05-20T14:01:44.39Z" }, + { url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" }, + { url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998, upload-time = "2026-05-20T14:09:16.912Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470, upload-time = "2026-05-20T14:01:45.611Z" }, + { url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" }, + { url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" }, + { url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" }, +] + +[[package]] +name = "playwright" +version = "1.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/f0/832bd9677194908da118064eef20082f2791e3d18215cc6d9391ee2c5a67/playwright-1.60.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:6a8cd0fec171fb3089e95e898c8bc8a6f35dea0b78b399e12fcc19427e91b1d7", size = 43474635, upload-time = "2026-05-18T12:00:31.969Z" }, + { url = "https://files.pythonhosted.org/packages/59/7b/e1d32ae8a3ed937ec2be3721c5f728b13d731a0b7c6442e0b3bec5094ac0/playwright-1.60.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:39b5420ba6145045b69ced4c5c47d4d9fe5bddfc8ff816c518913afcb25ec7a5", size = 42261327, upload-time = "2026-05-18T12:00:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bc/23de499ded6411c188a20c5a0dea6f0cd4ed5d2b3cc6042a5dbd3ed609aa/playwright-1.60.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:2581d0e6a3392c71f91b27460c7fd093356818dc430f48153896c8aeeaef7705", size = 43474636, upload-time = "2026-05-18T12:00:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/22/7b/1d679f4fced4ea94efadd17103856d8c565384f68382a1681264e46f5925/playwright-1.60.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:1c2bfae7884fb3fb05b853290eab8f343d524e5016f2f1def702acbbdf14c93e", size = 47467220, upload-time = "2026-05-18T12:00:43.179Z" }, + { url = "https://files.pythonhosted.org/packages/84/c2/1528d267d4442bd2c6b8eaeab819dd52c2030bf80e89293f0ba1f687473b/playwright-1.60.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43e66564125ee31b07a58cefb21e256d62d67d8d1713e6858df7a3019d8ed353", size = 47154856, upload-time = "2026-05-18T12:00:46.715Z" }, + { url = "https://files.pythonhosted.org/packages/bb/4e/b008b6440a7a1624378041da94829956d4b8f7ab9ef5aad22d0dc3f2e26d/playwright-1.60.0-py3-none-win32.whl", hash = "sha256:ec94e416ea320711e0ad4bf185dcbf41833672961e90773e1885255d7db7b7e7", size = 37902157, upload-time = "2026-05-18T12:00:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/55/f0/0541524133104f9cc20bf900870ff4a736b76a23483f3a55295ddfa58409/playwright-1.60.0-py3-none-win_amd64.whl", hash = "sha256:9566821ce6030a1f9e7146a24e19355ab0d98805fd0f9be50bb3d8fef1750c02", size = 37902159, upload-time = "2026-05-18T12:00:53.728Z" }, + { url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" }, +] + +[[package]] +name = "pyee" +version = "13.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 09f36db29f63da5e18921c72ece43bbe11f9eb95 Mon Sep 17 00:00:00 2001 From: Ishankoradia Date: Fri, 5 Jun 2026 12:31:02 +0530 Subject: [PATCH 2/2] updates --- .claude/settings.json | 23 +++++++++++++++++++++++ docs/domain-map.md | 12 +++++------- scripts/recipes/kpis.yaml | 5 +++-- scripts/recipes/metrics.yaml | 4 +++- 4 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..5ffb228 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,23 @@ +{ + "permissions": { + "allow": [ + "Read", + "Bash(ls *)" + ], + "deny": [ + "Read(**/.env*)", + "Bash(cat .env*)", + "Bash(cat **/.env*)", + "Bash(grep *.env*)", + "Bash(grep * .env*)", + "Bash(grep * **/.env*)", + "Bash(rg *.env*)", + "Bash(rg * .env*)", + "Bash(rg * **/.env*)", + "Bash(head *.env*)", + "Bash(tail *.env*)", + "Bash(less *.env*)", + "Bash(more *.env*)" + ] + } +} diff --git a/docs/domain-map.md b/docs/domain-map.md index 3e388fb..d4275df 100644 --- a/docs/domain-map.md +++ b/docs/domain-map.md @@ -151,7 +151,7 @@ Only 1-hop edges are listed per entity. Transitive paths (Metric → Dashboard - **Change impact:** Adding "Saved Metrics" tab to MetricsSelector; existing ad-hoc behavior unchanged. - **Confidence:** `draft` -### Metric *(arriving in v1 of the Metrics & KPIs spec)* +### Metric - **One-line identity:** A named, saved aggregation (e.g. "Active Students") — defined once in the library, referenced from Charts, KPIs, and Alerts. - **What it is (detail):** New DB model per the Metrics & KPIs spec. Two definition paths (mutually exclusive): Simple (`column` + `aggregation` via dropdowns) or Expression (`column_expression` — free-text for complex aggregations). No filters, no tags. Validated on save by executing a test query against the warehouse. Serialized via `MetricSchema` for API responses; converted to `ChartMetric` when used in chart query execution. - **Consumes:** Warehouse (`query-from`), Transform (`query-from`). @@ -164,9 +164,9 @@ Only 1-hop edges are listed per entity. Transitive paths (Metric → Dashboard - **One Metric entity in the codebase.** The existing inline chart config shape (`column + aggregation + alias`) is `ChartMetric`. `Metric` is the persisted, reusable entity; `ChartMetric` is the inline, per-chart shape. When a chart uses a saved Metric, the resolution path is: `saved_metric_id` → `Metric` DB row → `MetricSchema` → `ChartMetric`. - Delete-blocked if consumers exist (Charts with `saved_metric_id` or KPIs with FK). - **Change impact:** Column/aggregation change flows live to every consumer on next evaluation. Renames are safe if consumers reference by ID. Deletion is blocked until consumers are removed. -- **Confidence:** `draft` — v1 has shipped (routes `/metrics` exist with Simple + Calculated modes, blast-radius confirmation, delete-blocked behavior). Promote to `verified` after reading `models/metric.py`. +- **Confidence:** `verified` (read `models/metric.py` — `Metric` model with Simple `column + aggregation` and Calculated `column_expression` paths; `unique_metric_name_per_org` constraint; KPI FK uses `on_delete=PROTECT` for delete-blocking; consumer-check API at `/api/metric/{id}/consumers/` returns Charts via `saved_metric_id` and KPIs via FK). -### KPI *(arriving in v1 of the Metrics & KPIs spec)* +### KPI - **One-line identity:** A Metric wrapped with target + direction + RAG thresholds + trendline; leadership-facing. - **What it is (detail):** New model. Has Metric FK (`on_delete=PROTECT`), target, direction (increase/decrease), green/amber thresholds, time grain (daily/weekly/monthly/quarterly/yearly), trend periods, metric-type tag (Input/Output/Outcome/Impact). - **Consumes:** Metric (`reference` — required FK). @@ -182,7 +182,7 @@ Only 1-hop edges are listed per entity. Transitive paths (Metric → Dashboard - **Detail drawer surfaces an Annotations timeline** — comments and beneficiary quotes added by the team, grouped by period with delta-since-last-period. This is unique to KPI (not present on Chart or Metric) and is a primary leadership-review surface. - Linked Metric, Time Column, and Time Grain are **immutable post-create**. To change any, delete + recreate. - **Change impact:** Target change recolors historical RAG — note on backdating. Threshold change affects Alert fire rate. KPI value/target changes appear live on dashboards and live share links, but NOT in already-captured ReportSnapshots (frozen). -- **Confidence:** `draft` — v1 has shipped (routes `/kpis` exist with two-step form, RAG threshold logic, time-grain selector, annotations drawer). Promote to `verified` after reading `models/kpi.py`. +- **Confidence:** `verified` (read `models/metric.py` — `KPI` model has Metric FK with `on_delete=PROTECT`, `target_value` (nullable), `direction` (increase/decrease), `green_threshold_pct` (default 100) + `amber_threshold_pct` (default 80) — red auto-computed, `time_grain` enum (daily/weekly/monthly/quarterly/yearly), `time_dimension_column`, `metric_type_tag` enum (input/output/outcome/impact), `program_tags` JSON list, `annotations` JSON list on the KPI row itself). ### Dashboard - **One-line identity:** A user-composed canvas of Charts + text/heading blocks, with filters, optionally published for public viewing. @@ -359,11 +359,9 @@ Update order for the next team review session: - Warehouse (org config + adapter layer) - Transform (`models/dbt_workflow.py`, `ddpdbt/`) - Pipeline (`ddpprefect/`, `models/flow_runs.py`) - - Metric (`models/metric.py` — confirm Simple vs Calculated, validation path) - - KPI (`models/kpi.py` — confirm RAG storage, annotations table, immutability rules) - Organization, OrgUser 3. `verified` entries only need re-check on model changes: - - Chart, Dashboard, ReportSnapshot, Share link (both modes), Notification + - Chart, Dashboard, ReportSnapshot, Share link (both modes), Notification, Metric, KPI ### What is intentionally NOT in this map diff --git a/scripts/recipes/kpis.yaml b/scripts/recipes/kpis.yaml index 983706b..ed97fdc 100644 --- a/scripts/recipes/kpis.yaml +++ b/scripts/recipes/kpis.yaml @@ -2,9 +2,10 @@ feature: kpis output_dir: kpis nuances: | - KPI card has data-testid="kpi-card-{id}" (dynamic per row) — use prefix match. - - Create KPI dialog is two-step; without selecting a metric on step 1 - you cannot reach step 2 — only step 1 is captured here. + - Create KPI dialog is two-step. Step 1 needs a metric + name + target + + direction before Continue is enabled; only step 1 is captured here. - Detail drawer opens from clicking a KPI card; requires at least one KPI on staging. + - Drawer width is 600px and slides in from the right. flows: - name: list diff --git a/scripts/recipes/metrics.yaml b/scripts/recipes/metrics.yaml index db65b7f..101d998 100644 --- a/scripts/recipes/metrics.yaml +++ b/scripts/recipes/metrics.yaml @@ -2,8 +2,10 @@ feature: metrics output_dir: metrics nuances: | - "Create Metric" opens a dialog defaulting to Simple Mode. - - The Calculated Mode switcher may be a Tab or a Switch component; unverified. + - Simple / Calculated is a tab control inside the dialog. - Search bar at top filters by name; not captured. + - Datasource picker is a two-step popover (schema, then table); first row + suffices for screenshot. flows: - name: list