@amaster.ai/pi-lark 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/skills/lark-apps/SKILL.md +59 -14
- package/skills/lark-apps/creative-design/agents/assets/vision-probe.png +0 -0
- package/skills/lark-apps/creative-design/agents/fork-verifier-agent.md +71 -0
- package/skills/lark-apps/creative-design/agents/vision-probe-agent.md +41 -0
- package/skills/lark-apps/creative-design/assets/index.html +27 -0
- package/skills/lark-apps/creative-design/creative-design.md +239 -0
- package/skills/lark-apps/creative-design/references/aily.md +39 -0
- package/skills/lark-apps/creative-design/references/animated-video.md +34 -0
- package/skills/lark-apps/creative-design/references/charts.md +165 -0
- package/skills/lark-apps/creative-design/references/claude.md +36 -0
- package/skills/lark-apps/creative-design/references/codex.md +32 -0
- package/skills/lark-apps/creative-design/references/data-report.md +108 -0
- package/skills/lark-apps/creative-design/references/frontend-design.md +71 -0
- package/skills/lark-apps/creative-design/references/hi-fi-design.md +32 -0
- package/skills/lark-apps/creative-design/references/interactive-prototype.md +24 -0
- package/skills/lark-apps/creative-design/references/make-a-deck.md +133 -0
- package/skills/lark-apps/creative-design/references/visual-exposure.md +82 -0
- package/skills/lark-apps/creative-design/references/wireframe.md +14 -0
- package/skills/lark-apps/creative-design/starter-components/android-frame.jsx +188 -0
- package/skills/lark-apps/creative-design/starter-components/animations.jsx +773 -0
- package/skills/lark-apps/creative-design/starter-components/browser-window.jsx +122 -0
- package/skills/lark-apps/creative-design/starter-components/deck-stage.js +2483 -0
- package/skills/lark-apps/creative-design/starter-components/design-canvas.jsx +1432 -0
- package/skills/lark-apps/creative-design/starter-components/ios-frame.jsx +270 -0
- package/skills/lark-apps/creative-design/starter-components/macos-window.jsx +197 -0
- package/skills/lark-apps/creative-design/starter-components/tweaks-panel.jsx +752 -0
- package/skills/lark-apps/references/lark-apps-automation.md +80 -2
- package/skills/lark-apps/references/lark-apps-cache.md +61 -0
- package/skills/lark-apps/references/lark-apps-cloud-dev.md +5 -5
- package/skills/lark-apps/references/lark-apps-create.md +6 -4
- package/skills/lark-apps/references/lark-apps-db.md +1 -1
- package/skills/lark-apps/references/lark-apps-env-pull.md +1 -1
- package/skills/lark-apps/references/lark-apps-file.md +2 -2
- package/skills/lark-apps/references/lark-apps-get.md +1 -1
- package/skills/lark-apps/references/lark-apps-git-credential.md +1 -1
- package/skills/lark-apps/references/lark-apps-html-publish.md +4 -8
- package/skills/lark-apps/references/lark-apps-init.md +1 -1
- package/skills/lark-apps/references/lark-apps-list.md +2 -2
- package/skills/lark-apps/references/lark-apps-local-dev.md +80 -11
- package/skills/lark-apps/references/lark-apps-openapi-key.md +1 -1
- package/skills/lark-apps/references/lark-apps-release-create.md +2 -2
- package/skills/lark-apps/references/lark-apps-release-get.md +3 -3
- package/skills/lark-base/SKILL.md +34 -19
- package/skills/lark-base/references/lark-base-cell-value.md +3 -3
- package/skills/lark-base/references/lark-base-dashboard-block-get-data.md +17 -1
- package/skills/lark-base/references/lark-base-dashboard.md +17 -4
- package/skills/lark-base/references/lark-base-data-query-guide.md +8 -0
- package/skills/lark-base/references/lark-base-data-query.md +11 -4
- package/skills/lark-base/references/lark-base-field-create.md +21 -6
- package/skills/lark-base/references/lark-base-field-json.md +9 -6
- package/skills/lark-base/references/lark-base-field-update.md +17 -1
- package/skills/lark-base/references/lark-base-filter-condition.md +179 -0
- package/skills/lark-base/references/lark-base-form-questions-create.md +40 -7
- package/skills/lark-base/references/lark-base-form-questions-update.md +73 -20
- package/skills/lark-base/references/lark-base-form-submit.md +16 -7
- package/skills/lark-base/references/lark-base-record-batch-create.md +12 -10
- package/skills/lark-base/references/lark-base-record-batch-update.md +11 -9
- package/skills/lark-base/references/lark-base-record-upsert.md +1 -1
- package/skills/lark-base/references/lark-base-role-guide.md +11 -0
- package/skills/lark-base/references/lark-base-view-set-filter.md +11 -137
- package/skills/lark-base/references/role-config.md +31 -5
- package/skills/lark-calendar/SKILL.md +14 -8
- package/skills/lark-calendar/references/lark-calendar-create.md +6 -5
- package/skills/lark-calendar/references/lark-calendar-recurring.md +1 -0
- package/skills/lark-calendar/references/lark-calendar-room-find.md +2 -1
- package/skills/lark-calendar/references/lark-calendar-schedule-clear-time.md +1 -0
- package/skills/lark-calendar/references/lark-calendar-suggestion.md +1 -1
- package/skills/lark-calendar/references/lark-calendar-update.md +10 -4
- package/skills/lark-contact/SKILL.md +19 -3
- package/skills/lark-contact/references/lark-contact-search-bot.md +60 -0
- package/skills/lark-doc/SKILL.md +26 -61
- package/skills/lark-doc/references/genres/business-analysis.md +30 -0
- package/skills/lark-doc/references/genres/data-report.md +32 -0
- package/skills/lark-doc/references/genres/email.md +38 -0
- package/skills/lark-doc/references/genres/execution-plan.md +27 -0
- package/skills/lark-doc/references/genres/formal-doc.md +37 -0
- package/skills/lark-doc/references/genres/meeting-minutes.md +24 -0
- package/skills/lark-doc/references/genres/memo-brief.md +25 -0
- package/skills/lark-doc/references/genres/official-redhead.md +73 -0
- package/skills/lark-doc/references/genres/prd.md +26 -0
- package/skills/lark-doc/references/genres/proposal.md +24 -0
- package/skills/lark-doc/references/genres/research-report.md +32 -0
- package/skills/lark-doc/references/genres/retrospective.md +25 -0
- package/skills/lark-doc/references/genres/route-consumer.md +37 -0
- package/skills/lark-doc/references/genres/route-creative.md +36 -0
- package/skills/lark-doc/references/genres/route-knowledge.md +39 -0
- package/skills/lark-doc/references/genres/route-marketing.md +40 -0
- package/skills/lark-doc/references/genres/route-media.md +36 -0
- package/skills/lark-doc/references/genres/route-opinion.md +38 -0
- package/skills/lark-doc/references/genres/route-personal-brand.md +36 -0
- package/skills/lark-doc/references/genres/route-platform.md +9 -0
- package/skills/lark-doc/references/genres/route-report.md +10 -0
- package/skills/lark-doc/references/genres/route-workplace.md +17 -0
- package/skills/lark-doc/references/genres/sop-tutorial.md +41 -0
- package/skills/lark-doc/references/genres/technical-doc.md +39 -0
- package/skills/lark-doc/references/genres/wechat.md +39 -0
- package/skills/lark-doc/references/genres/weekly-report.md +24 -0
- package/skills/lark-doc/references/genres/white-paper.md +32 -0
- package/skills/lark-doc/references/genres/xiaohongshu.md +38 -0
- package/skills/lark-doc/references/lark-doc-create-workflow.md +121 -0
- package/skills/lark-doc/references/lark-doc-create.md +22 -48
- package/skills/lark-doc/references/lark-doc-fetch.md +84 -93
- package/skills/lark-doc/references/lark-doc-history.md +16 -15
- package/skills/lark-doc/references/lark-doc-md.md +5 -1
- package/skills/lark-doc/references/lark-doc-media-download.md +2 -1
- package/skills/lark-doc/references/lark-doc-script.md +76 -0
- package/skills/lark-doc/references/lark-doc-update.md +70 -222
- package/skills/lark-doc/references/lark-doc-whiteboard.md +14 -17
- package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +46 -0
- package/skills/lark-doc/references/lark-doc-xml.md +38 -166
- package/skills/lark-drive/SKILL.md +32 -50
- package/skills/lark-drive/references/lark-drive-add-comment.md +2 -4
- package/skills/lark-drive/references/lark-drive-add-reply.md +47 -0
- package/skills/lark-drive/references/lark-drive-apply-permission.md +3 -3
- package/skills/lark-drive/references/lark-drive-batch-query-comments.md +46 -0
- package/skills/lark-drive/references/lark-drive-comment-content.md +50 -0
- package/skills/lark-drive/references/lark-drive-comment-location.md +9 -15
- package/skills/lark-drive/references/lark-drive-copy.md +87 -0
- package/skills/lark-drive/references/lark-drive-delete-reply.md +48 -0
- package/skills/lark-drive/references/lark-drive-download.md +6 -1
- package/skills/lark-drive/references/lark-drive-export.md +3 -0
- package/skills/lark-drive/references/lark-drive-list-comments.md +25 -68
- package/skills/lark-drive/references/lark-drive-list-replies.md +54 -0
- package/skills/lark-drive/references/lark-drive-member-add.md +2 -2
- package/skills/lark-drive/references/lark-drive-member-list.md +65 -0
- package/skills/lark-drive/references/lark-drive-permission-get-setting.md +48 -0
- package/skills/lark-drive/references/lark-drive-preview.md +11 -1
- package/skills/lark-drive/references/lark-drive-react-reply.md +51 -0
- package/skills/lark-drive/references/lark-drive-reactions.md +27 -25
- package/skills/lark-drive/references/lark-drive-resolve-comment.md +45 -0
- package/skills/lark-drive/references/lark-drive-restore-comment.md +46 -0
- package/skills/lark-drive/references/lark-drive-search.md +7 -1
- package/skills/lark-drive/references/lark-drive-secure-label.md +1 -1
- package/skills/lark-drive/references/lark-drive-task-result.md +3 -0
- package/skills/lark-drive/references/lark-drive-update-reply.md +46 -0
- package/skills/lark-drive/references/lark-drive-update-title.md +78 -0
- package/skills/lark-drive/references/lark-drive-upload.md +1 -0
- package/skills/lark-drive/references/lark-drive-workflow-permission-governance-commands.md +38 -8
- package/skills/lark-drive/references/lark-drive-workflow-permission-governance-outputs.md +10 -10
- package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +22 -20
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-execute.md +273 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-recall.md +202 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-resolve-verify.md +231 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-review-plan.md +248 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-setup.md +174 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector.md +202 -0
- package/skills/lark-drive/references/lark-drive-workflow.md +5 -4
- package/skills/lark-event/SKILL.md +8 -4
- package/skills/lark-event/references/lark-event-application.md +38 -0
- package/skills/lark-event/references/lark-event-vc.md +8 -2
- package/skills/lark-im/SKILL.md +9 -9
- package/skills/lark-im/references/card/card-2.0-schema.md +1 -1
- package/skills/lark-im/references/card/lark-im-card-style.md +4 -4
- package/skills/lark-im/references/card/resource/icons.md +14 -0
- package/skills/lark-im/references/lark-im-chat-list.md +9 -2
- package/skills/lark-im/references/lark-im-chat-members-list.md +7 -4
- package/skills/lark-im/references/lark-im-chat-messages-list.md +10 -3
- package/skills/lark-im/references/lark-im-chat-search.md +9 -2
- package/skills/lark-im/references/lark-im-feed-group-list-item.md +2 -2
- package/skills/lark-im/references/lark-im-feed-group-list.md +2 -2
- package/skills/lark-im/references/lark-im-feed-shortcut-list.md +1 -1
- package/skills/lark-im/references/lark-im-flag-list.md +9 -8
- package/skills/lark-im/references/lark-im-message-enrichment.md +1 -1
- package/skills/lark-im/references/lark-im-messages-resources-download.md +19 -25
- package/skills/lark-im/references/lark-im-messages-search.md +4 -5
- package/skills/lark-im/references/lark-im-threads-messages-list.md +8 -4
- package/skills/lark-mail/references/lark-mail-triage.md +19 -4
- package/skills/lark-minutes/SKILL.md +1 -1
- package/skills/lark-minutes/references/lark-minutes-search.md +6 -7
- package/skills/lark-okr/SKILL.md +71 -26
- package/skills/lark-okr/references/lark-okr-batch-create.md +19 -18
- package/skills/lark-okr/references/lark-okr-create.md +173 -0
- package/skills/lark-okr/references/lark-okr-cycle-list.md +17 -7
- package/skills/lark-okr/references/lark-okr-entities.md +1 -0
- package/skills/lark-okr/references/lark-okr-indicator-update.md +3 -1
- package/skills/lark-okr/references/lark-okr-indicators.md +61 -12
- package/skills/lark-okr/references/lark-okr-progress-list.md +21 -9
- package/skills/lark-shared/SKILL.md +3 -3
- package/skills/lark-sheets/SKILL.md +83 -82
- package/skills/lark-sheets/references/lark-sheets-batch-update.md +13 -58
- package/skills/lark-sheets/references/lark-sheets-chart.md +2 -1
- package/skills/lark-sheets/references/lark-sheets-conditional-format.md +1 -1
- package/skills/lark-sheets/references/lark-sheets-range-operations.md +5 -5
- package/skills/lark-sheets/references/lark-sheets-read-data.md +80 -6
- package/skills/lark-sheets/references/lark-sheets-sheet-structure.md +21 -10
- package/skills/lark-sheets/references/lark-sheets-styles-put.md +93 -0
- package/skills/lark-sheets/references/lark-sheets-visual-standards.md +2 -2
- package/skills/lark-sheets/references/lark-sheets-workbook.md +4 -3
- package/skills/lark-sheets/references/lark-sheets-write-cells.md +40 -12
- package/skills/lark-sheets/scripts/lark_detect_subtables.py +593 -0
- package/skills/lark-sheets/scripts/lark_inspect_workbook.py +188 -0
- package/skills/lark-sheets/scripts/lark_profile_table.py +614 -0
- package/skills/lark-sheets/scripts/lark_sheet_range.py +176 -0
- package/skills/lark-sheets/scripts/lark_sheet_read_cli.py +184 -0
- package/skills/lark-sheets/scripts/sheets_df.py +21 -3
- package/skills/lark-slides/SKILL.md +134 -104
- package/skills/lark-slides/references/asset-planning.md +6 -4
- package/skills/lark-slides/references/iconpark.md +2 -2
- package/skills/lark-slides/references/lark-slides-add-slide.md +92 -0
- package/skills/lark-slides/references/lark-slides-create.md +86 -66
- package/skills/lark-slides/references/lark-slides-delete-slide.md +65 -0
- package/skills/lark-slides/references/lark-slides-edit-workflows.md +6 -7
- package/skills/lark-slides/references/lark-slides-history.md +132 -0
- package/skills/lark-slides/references/lark-slides-media-upload.md +4 -27
- package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +7 -11
- package/skills/lark-slides/references/lark-slides-replace-slide.md +22 -4
- package/skills/lark-slides/references/lark-slides-screenshot.md +33 -15
- package/skills/lark-slides/references/lark-slides-update-slide.md +146 -0
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +2 -2
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +2 -3
- package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +90 -32
- package/skills/lark-slides/references/planning-layer.md +11 -10
- package/skills/lark-slides/references/slides_chart_demo.xml +1415 -1
- package/skills/lark-slides/references/slides_xml_schema_definition.xml +539 -79
- package/skills/lark-slides/references/troubleshooting.md +26 -9
- package/skills/lark-slides/references/validation-checklist.md +55 -18
- package/skills/lark-slides/references/visual-planning.md +25 -22
- package/skills/lark-slides/references/xml-schema-quick-ref.md +299 -51
- package/skills/lark-slides/scripts/sxsd_validator.py +1052 -0
- package/skills/lark-slides/scripts/xml_text_overlap_lint.py +1964 -195
- package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +4051 -501
- package/skills/lark-task/SKILL.md +7 -0
- package/skills/lark-task/references/lark-task-complete.md +6 -2
- package/skills/lark-task/references/lark-task-create.md +9 -0
- package/skills/lark-task/references/lark-task-update.md +6 -2
- package/skills/lark-whiteboard/SKILL.md +21 -13
- package/skills/lark-whiteboard/elements/layout.md +1 -1
- package/skills/lark-whiteboard/elements/schema.md +2 -2
- package/skills/lark-whiteboard/references/{lark-whiteboard-query.md → lark-whiteboard-export.md} +17 -16
- package/skills/lark-whiteboard/references/lark-whiteboard-update.md +7 -7
- package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +23 -31
- package/skills/lark-whiteboard/routes/dsl.md +11 -5
- package/skills/lark-whiteboard/routes/mermaid.md +3 -3
- package/skills/lark-whiteboard/routes/svg-edit.md +9 -6
- package/skills/lark-whiteboard/routes/svg.md +14 -7
- package/skills/lark-whiteboard/scenes/bar-chart.md +1 -1
- package/skills/lark-whiteboard/scenes/fishbone.md +1 -1
- package/skills/lark-whiteboard/scenes/flywheel.md +1 -1
- package/skills/lark-whiteboard/scenes/line-chart.md +1 -1
- package/skills/lark-whiteboard/scenes/mention.md +71 -0
- package/skills/lark-whiteboard/scenes/treemap.md +1 -1
- package/skills/lark-wiki/SKILL.md +6 -3
- package/skills/lark-wiki/references/lark-wiki-delete-space.md +6 -3
- package/skills/lark-doc/references/lark-doc-word-stat.md +0 -93
- package/skills/lark-doc/references/style/lark-doc-create-workflow.md +0 -47
- package/skills/lark-doc/references/style/lark-doc-style.md +0 -68
- package/skills/lark-doc/references/style/lark-doc-update-workflow.md +0 -48
- package/skills/lark-doc/scripts/doc_word_stat.py +0 -1243
- package/skills/lark-drive/references/lark-drive-comments-guide.md +0 -80
- package/skills/lark-slides/references/examples.md +0 -91
- package/skills/lark-slides/references/lark-slides-replace-pages.md +0 -95
- package/skills/lark-slides/references/lark-slides-whiteboard.md +0 -331
- package/skills/lark-slides/references/lark-slides-xml-get.md +0 -100
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +0 -125
- package/skills/lark-slides/references/slide-templates.md +0 -201
- package/skills/lark-slides/references/slides_demo.xml +0 -226
- package/skills/lark-slides/references/xml-format-guide.md +0 -433
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""Profile a candidate table range in a Lark sheet."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from lark_sheet_range import col_to_index, format_range, index_to_col, parse_range
|
|
13
|
+
from lark_sheet_read_cli import (
|
|
14
|
+
LarkCliError,
|
|
15
|
+
add_spreadsheet_args,
|
|
16
|
+
emit_error,
|
|
17
|
+
emit_success,
|
|
18
|
+
envelope_data,
|
|
19
|
+
run_sheets,
|
|
20
|
+
)
|
|
21
|
+
from lark_detect_subtables import CsvGrid, parse_annotated_csv
|
|
22
|
+
|
|
23
|
+
ACTION = "profile_table"
|
|
24
|
+
ERROR_VALUES = ("#VALUE!", "#DIV/0!", "#REF!", "#NAME?", "#NULL!", "#NUM!", "#N/A")
|
|
25
|
+
TOTAL_KEYWORDS = ("合计", "总计", "小计", "汇总", "累计")
|
|
26
|
+
TOTAL_EN_RE = re.compile(r"\b(?:grand total|subtotal|total)\b", re.IGNORECASE)
|
|
27
|
+
SIGNATURE_KEYWORDS = ("编制人", "审核人", "审批人", "负责人", "经理", "签名")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_number(value: str) -> bool:
|
|
31
|
+
text = value.strip().replace(",", "")
|
|
32
|
+
if text.endswith("%"):
|
|
33
|
+
text = text[:-1]
|
|
34
|
+
try:
|
|
35
|
+
float(text)
|
|
36
|
+
return True
|
|
37
|
+
except ValueError:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_date_like(value: str) -> bool:
|
|
42
|
+
text = value.strip()
|
|
43
|
+
if re.match(r"^\d{4}[-/]\d{1,2}[-/]\d{1,2}(?:\s+\d{1,2}:\d{2}(:\d{2})?)?$", text):
|
|
44
|
+
return True
|
|
45
|
+
if re.match(r"^\d{8}$", text):
|
|
46
|
+
year = int(text[:4])
|
|
47
|
+
month = int(text[4:6])
|
|
48
|
+
day = int(text[6:8])
|
|
49
|
+
return 1900 <= year <= 2100 and 1 <= month <= 12 and 1 <= day <= 31
|
|
50
|
+
if re.match(r"^\d{6}$", text):
|
|
51
|
+
year = int(text[:4])
|
|
52
|
+
month = int(text[4:6])
|
|
53
|
+
return 1900 <= year <= 2100 and 1 <= month <= 12
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _classify_value(value: str) -> str:
|
|
58
|
+
text = value.strip()
|
|
59
|
+
if not text:
|
|
60
|
+
return "empty"
|
|
61
|
+
if any(err in text for err in ERROR_VALUES):
|
|
62
|
+
return "error"
|
|
63
|
+
if re.match(r"^0\d+$", text) or re.match(r"^\d{11,}$", text):
|
|
64
|
+
return "string_id"
|
|
65
|
+
if _is_date_like(text):
|
|
66
|
+
return "date"
|
|
67
|
+
if _is_number(text):
|
|
68
|
+
return "number"
|
|
69
|
+
if text.lower() in {"true", "false", "yes", "no"}:
|
|
70
|
+
return "bool"
|
|
71
|
+
return "string"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _row_values(grid: CsvGrid, row_index: int) -> list[str]:
|
|
75
|
+
return grid.values[row_index]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _header_score(row: list[str]) -> tuple[int, int]:
|
|
79
|
+
non_empty = [value for value in row if value.strip()]
|
|
80
|
+
if not non_empty:
|
|
81
|
+
return 0, 0
|
|
82
|
+
strings = sum(1 for value in non_empty if _classify_value(value) in {"string", "string_id"})
|
|
83
|
+
data_like = sum(
|
|
84
|
+
1 for value in non_empty if _classify_value(value) in {"number", "date", "bool"}
|
|
85
|
+
)
|
|
86
|
+
return strings * 2 + len(non_empty) - data_like * 3, len(non_empty)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _header_candidate_score(grid: CsvGrid, index: int) -> int:
|
|
90
|
+
score, _ = _header_score(grid.values[index])
|
|
91
|
+
following = grid.values[index + 1 : index + 4]
|
|
92
|
+
following_non_empty = [value for row in following for value in row if value.strip()]
|
|
93
|
+
following_data_like = sum(
|
|
94
|
+
1 for value in following_non_empty if _classify_value(value) in {"number", "date", "bool"}
|
|
95
|
+
)
|
|
96
|
+
# A schema row is usually followed by data-shaped values; use this as a
|
|
97
|
+
# small tie-breaker while preferring earlier candidates with equal evidence.
|
|
98
|
+
score += min(3, following_data_like)
|
|
99
|
+
return score - index // 2
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def detect_header_row(grid: CsvGrid, scan_rows: int = 20) -> int | None:
|
|
103
|
+
best_row = None
|
|
104
|
+
best_score = None
|
|
105
|
+
for idx, row in enumerate(grid.values[:scan_rows]):
|
|
106
|
+
_, non_empty_count = _header_score(row)
|
|
107
|
+
if not non_empty_count:
|
|
108
|
+
continue
|
|
109
|
+
score = _header_candidate_score(grid, idx)
|
|
110
|
+
if best_score is None or score > best_score:
|
|
111
|
+
best_score = score
|
|
112
|
+
best_row = grid.row_numbers[idx]
|
|
113
|
+
return best_row
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _possible_multi_row_header(grid: CsvGrid, header_row: int | None) -> bool:
|
|
117
|
+
if header_row is None or header_row not in grid.row_numbers:
|
|
118
|
+
return False
|
|
119
|
+
header_index = grid.row_numbers.index(header_row)
|
|
120
|
+
if header_index + 1 >= len(grid.values):
|
|
121
|
+
return False
|
|
122
|
+
header_score, header_non_empty = _header_score(grid.values[header_index])
|
|
123
|
+
next_score, next_non_empty = _header_score(grid.values[header_index + 1])
|
|
124
|
+
if header_non_empty < 2 or next_non_empty < 2:
|
|
125
|
+
return False
|
|
126
|
+
return next_score >= max(4, (header_score * 3 + 3) // 4)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _header_row_not_first(grid: CsvGrid, header_row: int | None) -> bool:
|
|
130
|
+
if header_row is None:
|
|
131
|
+
return False
|
|
132
|
+
first_non_empty = next(
|
|
133
|
+
(
|
|
134
|
+
row_num
|
|
135
|
+
for row_num, row in zip(grid.row_numbers, grid.values)
|
|
136
|
+
if any(value.strip() for value in row)
|
|
137
|
+
),
|
|
138
|
+
None,
|
|
139
|
+
)
|
|
140
|
+
return first_non_empty is not None and first_non_empty != header_row
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def detect_special_rows(grid: CsvGrid, header_row: int | None) -> list[dict[str, Any]]:
|
|
144
|
+
specials = []
|
|
145
|
+
for idx, row in enumerate(grid.values):
|
|
146
|
+
row_num = grid.row_numbers[idx]
|
|
147
|
+
if header_row is not None and row_num <= header_row:
|
|
148
|
+
continue
|
|
149
|
+
joined = " ".join(value.strip() for value in row if value.strip())
|
|
150
|
+
if not joined:
|
|
151
|
+
specials.append({"row": row_num, "reason": "empty_row", "sample": ""})
|
|
152
|
+
continue
|
|
153
|
+
if any(keyword in joined for keyword in TOTAL_KEYWORDS) or TOTAL_EN_RE.search(joined):
|
|
154
|
+
specials.append({"row": row_num, "reason": "total_row", "sample": joined[:120]})
|
|
155
|
+
elif any(keyword in joined for keyword in SIGNATURE_KEYWORDS):
|
|
156
|
+
specials.append({"row": row_num, "reason": "signature_row", "sample": joined[:120]})
|
|
157
|
+
return specials
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _last_data_row(grid: CsvGrid, header_row: int | None, specials: list[dict[str, Any]]) -> int | None:
|
|
161
|
+
special_rows = {item["row"] for item in specials if item["reason"] != "empty_row"}
|
|
162
|
+
for idx in range(len(grid.row_numbers) - 1, -1, -1):
|
|
163
|
+
row_num = grid.row_numbers[idx]
|
|
164
|
+
if header_row is not None and row_num <= header_row:
|
|
165
|
+
continue
|
|
166
|
+
if row_num in special_rows:
|
|
167
|
+
continue
|
|
168
|
+
if any(value.strip() for value in grid.values[idx]):
|
|
169
|
+
return row_num
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _column_values(grid: CsvGrid, col_index: int, start_row: int, end_row: int | None) -> list[str]:
|
|
174
|
+
values = []
|
|
175
|
+
for idx, row_num in enumerate(grid.row_numbers):
|
|
176
|
+
if row_num < start_row:
|
|
177
|
+
continue
|
|
178
|
+
if end_row is not None and row_num > end_row:
|
|
179
|
+
continue
|
|
180
|
+
row = grid.values[idx]
|
|
181
|
+
values.append(row[col_index] if col_index < len(row) else "")
|
|
182
|
+
return values
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _type_guess(type_counts: dict[str, int]) -> str:
|
|
186
|
+
non_empty_counts = {key: value for key, value in type_counts.items() if key != "empty" and value}
|
|
187
|
+
if not non_empty_counts:
|
|
188
|
+
return "empty"
|
|
189
|
+
if len(non_empty_counts) == 1:
|
|
190
|
+
return next(iter(non_empty_counts))
|
|
191
|
+
if "error" in non_empty_counts:
|
|
192
|
+
return "mixed_with_errors"
|
|
193
|
+
if "string_id" in non_empty_counts and set(non_empty_counts) <= {"string_id", "string"}:
|
|
194
|
+
return "string_id"
|
|
195
|
+
return "mixed"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def profile_columns(
|
|
199
|
+
grid: CsvGrid,
|
|
200
|
+
*,
|
|
201
|
+
header_row: int | None,
|
|
202
|
+
data_start_row: int,
|
|
203
|
+
data_end_row: int | None,
|
|
204
|
+
) -> list[dict[str, Any]]:
|
|
205
|
+
header_values = []
|
|
206
|
+
if header_row in grid.row_numbers:
|
|
207
|
+
header_values = _row_values(grid, grid.row_numbers.index(header_row))
|
|
208
|
+
|
|
209
|
+
profiles = []
|
|
210
|
+
for idx, col in enumerate(grid.col_letters):
|
|
211
|
+
name = ""
|
|
212
|
+
if idx < len(header_values):
|
|
213
|
+
name = header_values[idx].strip()
|
|
214
|
+
if not name:
|
|
215
|
+
name = f"unnamed_{col}"
|
|
216
|
+
|
|
217
|
+
values = _column_values(grid, idx, data_start_row, data_end_row)
|
|
218
|
+
type_counts: dict[str, int] = {}
|
|
219
|
+
examples = []
|
|
220
|
+
error_count = 0
|
|
221
|
+
for value in values:
|
|
222
|
+
value_type = _classify_value(value)
|
|
223
|
+
type_counts[value_type] = type_counts.get(value_type, 0) + 1
|
|
224
|
+
if value_type == "error":
|
|
225
|
+
error_count += 1
|
|
226
|
+
if value.strip() and len(examples) < 5:
|
|
227
|
+
examples.append(value)
|
|
228
|
+
non_empty = len(values) - type_counts.get("empty", 0)
|
|
229
|
+
warnings = []
|
|
230
|
+
if type_counts.get("string_id"):
|
|
231
|
+
warnings.append("long_numeric_like_id")
|
|
232
|
+
if error_count:
|
|
233
|
+
warnings.append("formula_or_value_errors")
|
|
234
|
+
if non_empty and type_counts.get("empty", 0) / len(values) > 0.5:
|
|
235
|
+
warnings.append("many_empty_cells")
|
|
236
|
+
if _type_guess(type_counts) == "mixed":
|
|
237
|
+
warnings.append("mixed_value_types")
|
|
238
|
+
|
|
239
|
+
profiles.append(
|
|
240
|
+
{
|
|
241
|
+
"name": name,
|
|
242
|
+
"col": col,
|
|
243
|
+
"non_empty": non_empty,
|
|
244
|
+
"empty": type_counts.get("empty", 0),
|
|
245
|
+
"type_guess": _type_guess(type_counts),
|
|
246
|
+
"type_distribution": type_counts,
|
|
247
|
+
"examples": examples,
|
|
248
|
+
"warnings": warnings,
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
return profiles
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _field_map(columns: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
|
255
|
+
result: dict[str, list[dict[str, Any]]] = {}
|
|
256
|
+
for column in columns:
|
|
257
|
+
name = column["name"]
|
|
258
|
+
result.setdefault(name, []).append(
|
|
259
|
+
{
|
|
260
|
+
"col": column["col"],
|
|
261
|
+
"type_guess": column["type_guess"],
|
|
262
|
+
"warnings": column["warnings"],
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _risk_warnings(
|
|
269
|
+
*,
|
|
270
|
+
columns: list[dict[str, Any]],
|
|
271
|
+
special_rows: list[dict[str, Any]],
|
|
272
|
+
header_row: int | None,
|
|
273
|
+
data_range: str | None,
|
|
274
|
+
possible_multi_row_header: bool,
|
|
275
|
+
header_row_not_first: bool,
|
|
276
|
+
data_range_has_gaps: bool,
|
|
277
|
+
data_range_has_col_gaps: bool,
|
|
278
|
+
hidden_rows: list[int],
|
|
279
|
+
hidden_columns: list[str],
|
|
280
|
+
) -> list[str]:
|
|
281
|
+
warnings = {warning for column in columns for warning in column["warnings"]}
|
|
282
|
+
names = [column["name"] for column in columns]
|
|
283
|
+
if len(names) != len(set(names)):
|
|
284
|
+
warnings.add("duplicate_headers")
|
|
285
|
+
if any(name.startswith("unnamed_") for name in names):
|
|
286
|
+
warnings.add("unnamed_columns")
|
|
287
|
+
if header_row is None:
|
|
288
|
+
warnings.add("header_not_detected")
|
|
289
|
+
if data_range is None:
|
|
290
|
+
warnings.add("data_range_not_detected")
|
|
291
|
+
if any(item["reason"] != "empty_row" for item in special_rows):
|
|
292
|
+
warnings.add("special_rows_present")
|
|
293
|
+
if any(item["reason"] == "empty_row" for item in special_rows):
|
|
294
|
+
warnings.add("empty_rows_present")
|
|
295
|
+
if possible_multi_row_header:
|
|
296
|
+
warnings.add("possible_multi_row_header")
|
|
297
|
+
if header_row_not_first:
|
|
298
|
+
warnings.add("header_row_not_first")
|
|
299
|
+
if data_range_has_gaps:
|
|
300
|
+
warnings.add("data_range_has_gaps")
|
|
301
|
+
if data_range_has_col_gaps:
|
|
302
|
+
warnings.add("data_range_has_col_gaps")
|
|
303
|
+
if hidden_rows:
|
|
304
|
+
warnings.add("hidden_rows_in_range")
|
|
305
|
+
if hidden_columns:
|
|
306
|
+
warnings.add("hidden_columns_in_range")
|
|
307
|
+
return sorted(warnings)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _col_segments(col_letters: list[str]) -> list[list[str]]:
|
|
311
|
+
"""Group column letters into contiguous runs of real column indices.
|
|
312
|
+
|
|
313
|
+
With --skip-hidden the returned columns can be non-consecutive (A, C when B
|
|
314
|
+
is hidden). Reporting a single data_range across that gap would let a caller
|
|
315
|
+
write the hidden-column-free data back as if it were contiguous, shifting
|
|
316
|
+
every value right of the gap.
|
|
317
|
+
"""
|
|
318
|
+
if not col_letters:
|
|
319
|
+
return []
|
|
320
|
+
indices = [col_to_index(col) for col in col_letters]
|
|
321
|
+
segments = []
|
|
322
|
+
start = previous = indices[0]
|
|
323
|
+
for current in indices[1:]:
|
|
324
|
+
if current == previous + 1:
|
|
325
|
+
previous = current
|
|
326
|
+
continue
|
|
327
|
+
segments.append([index_to_col(start), index_to_col(previous)])
|
|
328
|
+
start = previous = current
|
|
329
|
+
segments.append([index_to_col(start), index_to_col(previous)])
|
|
330
|
+
return segments
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _row_segments(row_numbers: list[int]) -> list[list[int]]:
|
|
334
|
+
if not row_numbers:
|
|
335
|
+
return []
|
|
336
|
+
segments = []
|
|
337
|
+
start = end = row_numbers[0]
|
|
338
|
+
for row in row_numbers[1:]:
|
|
339
|
+
if row == end + 1:
|
|
340
|
+
end = row
|
|
341
|
+
continue
|
|
342
|
+
segments.append([start, end])
|
|
343
|
+
start = end = row
|
|
344
|
+
segments.append([start, end])
|
|
345
|
+
return segments
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _write_hints(
|
|
349
|
+
grid: CsvGrid,
|
|
350
|
+
*,
|
|
351
|
+
header_row: int | None,
|
|
352
|
+
data_start: int,
|
|
353
|
+
bounds,
|
|
354
|
+
hidden_columns: list[str] | None = None,
|
|
355
|
+
skip_hidden: bool = False,
|
|
356
|
+
) -> dict[str, Any]:
|
|
357
|
+
last_non_empty_col = None
|
|
358
|
+
for idx, col in enumerate(grid.col_letters):
|
|
359
|
+
if any(idx < len(row) and row[idx].strip() for row in grid.values):
|
|
360
|
+
last_non_empty_col = col_to_index(col)
|
|
361
|
+
safe_col_num = (last_non_empty_col + 1) if last_non_empty_col else bounds.start_col
|
|
362
|
+
# Step over hidden columns. Under --skip-hidden they are absent from the
|
|
363
|
+
# grid entirely, so "one past the last visible column" can land ON a hidden
|
|
364
|
+
# column that holds data — and when the hidden columns sit at the right edge
|
|
365
|
+
# there is no gap in the returned letters either, so data_range_has_col_gaps
|
|
366
|
+
# stays silent too. Appending there would overwrite data nobody can see.
|
|
367
|
+
hidden_indices = {col_to_index(col) for col in (hidden_columns or [])}
|
|
368
|
+
skipped_hidden: list[str] = []
|
|
369
|
+
while safe_col_num in hidden_indices:
|
|
370
|
+
skipped_hidden.append(index_to_col(safe_col_num))
|
|
371
|
+
safe_col_num += 1
|
|
372
|
+
safe_col = index_to_col(safe_col_num)
|
|
373
|
+
hints = {
|
|
374
|
+
"last_non_empty_col": index_to_col(last_non_empty_col) if last_non_empty_col else None,
|
|
375
|
+
}
|
|
376
|
+
if not (skip_hidden and hidden_columns is None):
|
|
377
|
+
hints.update(
|
|
378
|
+
{
|
|
379
|
+
"safe_append_col": safe_col,
|
|
380
|
+
"safe_append_header_cell": f"{safe_col}{header_row}" if header_row else None,
|
|
381
|
+
"safe_append_data_start_cell": f"{safe_col}{data_start}",
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
if skipped_hidden:
|
|
385
|
+
hints["skipped_hidden_cols"] = skipped_hidden
|
|
386
|
+
return hints
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def profile_grid(
|
|
390
|
+
grid: CsvGrid,
|
|
391
|
+
source_range: str,
|
|
392
|
+
*,
|
|
393
|
+
skip_hidden: bool = False,
|
|
394
|
+
hidden_rows: list[int] | None = None,
|
|
395
|
+
hidden_columns: list[str] | None = None,
|
|
396
|
+
all_hidden_columns: list[str] | None = None,
|
|
397
|
+
header_scan_rows: int = 20,
|
|
398
|
+
) -> dict[str, Any]:
|
|
399
|
+
max_row = max(grid.row_numbers, default=1)
|
|
400
|
+
max_col = max((col_to_index(col) for col in grid.col_letters), default=1)
|
|
401
|
+
bounds = parse_range(source_range, max_row=max_row, max_col=max_col)
|
|
402
|
+
header_row = detect_header_row(grid, header_scan_rows)
|
|
403
|
+
data_start = (header_row + 1) if header_row else bounds.start_row
|
|
404
|
+
specials = detect_special_rows(grid, header_row)
|
|
405
|
+
data_end = _last_data_row(grid, header_row, specials)
|
|
406
|
+
data_range = None
|
|
407
|
+
if data_end is not None and data_end >= data_start:
|
|
408
|
+
data_range = format_range(data_start, bounds.start_col, data_end, bounds.end_col)
|
|
409
|
+
columns = profile_columns(
|
|
410
|
+
grid,
|
|
411
|
+
header_row=header_row,
|
|
412
|
+
data_start_row=data_start,
|
|
413
|
+
data_end_row=data_end,
|
|
414
|
+
)
|
|
415
|
+
data_row_numbers = []
|
|
416
|
+
if data_end is not None and data_end >= data_start:
|
|
417
|
+
data_row_numbers = [
|
|
418
|
+
row for row in grid.row_numbers if data_start <= row <= data_end
|
|
419
|
+
]
|
|
420
|
+
data_row_segments = _row_segments(data_row_numbers)
|
|
421
|
+
data_range_has_gaps = bool(data_row_numbers) and (
|
|
422
|
+
data_row_numbers[0] != data_start or len(data_row_segments) > 1
|
|
423
|
+
)
|
|
424
|
+
data_rows = len(data_row_numbers)
|
|
425
|
+
data_col_segments = _col_segments(grid.col_letters)
|
|
426
|
+
data_range_has_col_gaps = len(data_col_segments) > 1
|
|
427
|
+
hidden_rows = hidden_rows or []
|
|
428
|
+
hidden_columns = hidden_columns or []
|
|
429
|
+
possible_multi_row_header = _possible_multi_row_header(grid, header_row)
|
|
430
|
+
header_row_not_first = _header_row_not_first(grid, header_row)
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
"summary": {
|
|
434
|
+
"range": source_range,
|
|
435
|
+
"header_row": header_row,
|
|
436
|
+
"data_range": data_range,
|
|
437
|
+
"data_rows": data_rows,
|
|
438
|
+
"data_row_segments": data_row_segments,
|
|
439
|
+
"data_col_segments": data_col_segments,
|
|
440
|
+
"column_count": len(columns),
|
|
441
|
+
"special_rows_count": len(specials),
|
|
442
|
+
},
|
|
443
|
+
"range": source_range,
|
|
444
|
+
"header_row": header_row,
|
|
445
|
+
"data_range": data_range,
|
|
446
|
+
"data_row_segments": data_row_segments,
|
|
447
|
+
"data_col_segments": data_col_segments,
|
|
448
|
+
"columns": columns,
|
|
449
|
+
"field_map": _field_map(columns),
|
|
450
|
+
"risk_warnings": _risk_warnings(
|
|
451
|
+
columns=columns,
|
|
452
|
+
special_rows=specials,
|
|
453
|
+
header_row=header_row,
|
|
454
|
+
data_range=data_range,
|
|
455
|
+
possible_multi_row_header=possible_multi_row_header,
|
|
456
|
+
header_row_not_first=header_row_not_first,
|
|
457
|
+
data_range_has_gaps=data_range_has_gaps,
|
|
458
|
+
data_range_has_col_gaps=data_range_has_col_gaps,
|
|
459
|
+
hidden_rows=hidden_rows,
|
|
460
|
+
hidden_columns=hidden_columns,
|
|
461
|
+
),
|
|
462
|
+
"visibility": {
|
|
463
|
+
"skip_hidden": skip_hidden,
|
|
464
|
+
"hidden_rows_in_range": hidden_rows,
|
|
465
|
+
"hidden_columns_in_range": hidden_columns,
|
|
466
|
+
},
|
|
467
|
+
"write_hints": _write_hints(
|
|
468
|
+
grid,
|
|
469
|
+
header_row=header_row,
|
|
470
|
+
data_start=data_start,
|
|
471
|
+
bounds=bounds,
|
|
472
|
+
hidden_columns=all_hidden_columns,
|
|
473
|
+
skip_hidden=skip_hidden,
|
|
474
|
+
),
|
|
475
|
+
"special_rows": specials,
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _hidden_rows_and_columns(grid: CsvGrid, layout: dict[str, Any]) -> tuple[list[int], list[str]]:
|
|
480
|
+
def indexes(key: str) -> set[int]:
|
|
481
|
+
values = layout.get(key)
|
|
482
|
+
if not isinstance(values, list):
|
|
483
|
+
return set()
|
|
484
|
+
result = set()
|
|
485
|
+
for value in values:
|
|
486
|
+
try:
|
|
487
|
+
result.add(int(value) + 1) # +sheet-info uses zero-based indices.
|
|
488
|
+
except (TypeError, ValueError):
|
|
489
|
+
continue
|
|
490
|
+
return result
|
|
491
|
+
|
|
492
|
+
hidden_row_indexes = indexes("hidden_rows")
|
|
493
|
+
hidden_columns = layout.get("hidden_cols") or layout.get("hidden_columns") or []
|
|
494
|
+
hidden_column_letters = set()
|
|
495
|
+
for value in hidden_columns if isinstance(hidden_columns, list) else []:
|
|
496
|
+
if isinstance(value, str) and value.isalpha():
|
|
497
|
+
hidden_column_letters.add(value.upper())
|
|
498
|
+
else:
|
|
499
|
+
try:
|
|
500
|
+
hidden_column_letters.add(index_to_col(int(value) + 1))
|
|
501
|
+
except (TypeError, ValueError):
|
|
502
|
+
continue
|
|
503
|
+
rows = sorted(row for row in grid.row_numbers if row in hidden_row_indexes)
|
|
504
|
+
columns = [col for col in grid.col_letters if col.upper() in hidden_column_letters]
|
|
505
|
+
return rows, columns
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _all_hidden_columns(layout: dict[str, Any]) -> list[str]:
|
|
509
|
+
"""Every hidden column on the sheet, not just those inside the grid.
|
|
510
|
+
|
|
511
|
+
_hidden_rows_and_columns intersects with the returned grid, which is the
|
|
512
|
+
right scope for the "there are hidden rows/columns in what you read"
|
|
513
|
+
warnings. The append hint needs the opposite: the columns that are NOT in
|
|
514
|
+
the grid precisely because they are hidden.
|
|
515
|
+
"""
|
|
516
|
+
letters: list[str] = []
|
|
517
|
+
raw = layout.get("hidden_cols") or layout.get("hidden_columns") or []
|
|
518
|
+
for value in raw if isinstance(raw, list) else []:
|
|
519
|
+
if isinstance(value, str) and value.isalpha():
|
|
520
|
+
letters.append(value.upper())
|
|
521
|
+
continue
|
|
522
|
+
try:
|
|
523
|
+
letters.append(index_to_col(int(value) + 1))
|
|
524
|
+
except (TypeError, ValueError):
|
|
525
|
+
continue
|
|
526
|
+
return letters
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def profile_table(args) -> tuple[dict[str, Any], list[str]]:
|
|
530
|
+
warnings = []
|
|
531
|
+
if args.header_scan_rows < 1:
|
|
532
|
+
raise ValueError("--header-scan-rows must be at least 1")
|
|
533
|
+
csv_data = envelope_data(
|
|
534
|
+
run_sheets(
|
|
535
|
+
"+csv-get",
|
|
536
|
+
url=args.url,
|
|
537
|
+
spreadsheet_token=args.spreadsheet_token,
|
|
538
|
+
sheet_id=args.sheet_id,
|
|
539
|
+
sheet_name=args.sheet_name,
|
|
540
|
+
flags={
|
|
541
|
+
"range": args.range,
|
|
542
|
+
"max_chars": args.max_chars,
|
|
543
|
+
"skip_hidden": True if args.skip_hidden else None,
|
|
544
|
+
},
|
|
545
|
+
timeout=args.timeout,
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
source_range = str(csv_data.get("actual_range") or args.range)
|
|
549
|
+
if csv_data.get("has_more"):
|
|
550
|
+
raise LarkCliError(
|
|
551
|
+
f"+csv-get truncated the requested range at {source_range}; narrow --range before profiling"
|
|
552
|
+
)
|
|
553
|
+
grid = parse_annotated_csv(
|
|
554
|
+
csv_data.get("annotated_csv", ""),
|
|
555
|
+
csv_data.get("col_indices"),
|
|
556
|
+
csv_data.get("row_indices"),
|
|
557
|
+
source_range,
|
|
558
|
+
)
|
|
559
|
+
if grid.row_numbers_inferred:
|
|
560
|
+
warnings.append("CSV row numbers were inferred from the requested range")
|
|
561
|
+
hidden_rows: list[int] = []
|
|
562
|
+
hidden_columns: list[str] = []
|
|
563
|
+
all_hidden_columns: list[str] | None = None
|
|
564
|
+
# Fetched in BOTH modes. Under --skip-hidden the hidden rows/columns are
|
|
565
|
+
# absent from the grid, so hidden_rows/hidden_columns (which are scoped to
|
|
566
|
+
# what the grid contains) come back empty and no warning fires — but the
|
|
567
|
+
# write hints still need to know where the hidden columns are, or
|
|
568
|
+
# safe_append_col can point at one that holds data. all_hidden_columns is
|
|
569
|
+
# the unscoped list used for exactly that.
|
|
570
|
+
try:
|
|
571
|
+
layout = envelope_data(
|
|
572
|
+
run_sheets(
|
|
573
|
+
"+sheet-info",
|
|
574
|
+
url=args.url,
|
|
575
|
+
spreadsheet_token=args.spreadsheet_token,
|
|
576
|
+
sheet_id=args.sheet_id,
|
|
577
|
+
sheet_name=args.sheet_name,
|
|
578
|
+
flags={"include": "hidden_rows,hidden_cols"},
|
|
579
|
+
timeout=args.timeout,
|
|
580
|
+
)
|
|
581
|
+
)
|
|
582
|
+
hidden_rows, hidden_columns = _hidden_rows_and_columns(grid, layout)
|
|
583
|
+
all_hidden_columns = _all_hidden_columns(layout)
|
|
584
|
+
except LarkCliError as exc:
|
|
585
|
+
warnings.append(f"hidden row/column detection unavailable: {exc}")
|
|
586
|
+
return profile_grid(
|
|
587
|
+
grid,
|
|
588
|
+
source_range,
|
|
589
|
+
skip_hidden=args.skip_hidden,
|
|
590
|
+
hidden_rows=hidden_rows,
|
|
591
|
+
hidden_columns=hidden_columns,
|
|
592
|
+
all_hidden_columns=all_hidden_columns,
|
|
593
|
+
header_scan_rows=args.header_scan_rows,
|
|
594
|
+
), warnings
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def main() -> None:
|
|
598
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
599
|
+
add_spreadsheet_args(parser, require_sheet=True, allow_sheet=True)
|
|
600
|
+
parser.add_argument("--range", required=True)
|
|
601
|
+
parser.add_argument("--max-chars", type=int, default=25000)
|
|
602
|
+
parser.add_argument("--header-scan-rows", type=int, default=20)
|
|
603
|
+
parser.add_argument("--skip-hidden", action="store_true")
|
|
604
|
+
parser.add_argument("--timeout", type=int, default=60)
|
|
605
|
+
args = parser.parse_args()
|
|
606
|
+
try:
|
|
607
|
+
data, warnings = profile_table(args)
|
|
608
|
+
except (LarkCliError, ValueError, TypeError) as exc:
|
|
609
|
+
emit_error(ACTION, str(exc))
|
|
610
|
+
emit_success(ACTION, data, warnings)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
if __name__ == "__main__":
|
|
614
|
+
main()
|