# Thực hành: biến script Python thành lệnh mà đội khách tự cài và tự chạy bằng Typer

> Script chạy được trên laptop của bạn vẫn chưa phải là thứ bàn giao được. Nó chỉ thành công cụ khi đội khách gõ một lệnh trên máy họ và chạy được ngay.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-cli-python-typer-dong-goi-cong-cu-cho-khach/

Tuần thứ ba ở site khách, bạn có một script `check_csv.py` để kiểm tra file dữ liệu trước khi nạp vào pipeline.

Đội vận hành của khách nhắn hỏi cách chạy, và bạn trả lời bằng một đoạn hướng dẫn dài: cài Python bản nào, `pip install` những gì, đặt file ở đâu.

Hai ngày sau họ báo lỗi vì máy họ đang có một thư viện khác phiên bản.

Code không sai. Thứ hỏng là cách bàn giao. Thứ FDE nên để lại là công cụ khách tự chạy được sau khi mình đã rời site, và với việc chạy bằng dòng lệnh thì đó là một CLI được đóng gói đàng hoàng. Năm bước dưới đây đưa một script rời thành lệnh `kiem-tra` mà đội khách cài chỉ bằng một dòng.

## Bạn sẽ dựng gì, và cần chuẩn bị gì?

Thành phẩm là một package Python tên `kiem-tra`. Package này có một lệnh nhận đường dẫn file CSV và tham số `--max-rows`, in kết quả ra màn hình và trả về mã lỗi khi file không đạt. Trên máy bạn cần có Python và [uv](https://docs.astral.sh/uv/). Phần logic kiểm tra trong ví dụ đã được lược bỏ, bạn thay bằng logic thật của mình.

Công cụ chính là Typer, thư viện tự giới thiệu là giúp xây CLI tốt, dễ viết và dựa trên type hint của Python.

Typer dựa trên type hint chuẩn của Python, nên bạn khai báo tham số giống như khi viết một hàm bình thường. Help và auto-completion cho Bash, Zsh, Fish, PowerShell thì có sẵn.

Đội khách được hưởng phần đó mà bạn không phải viết thêm dòng nào.

## Bước 1–2: tạo package và viết lệnh

Hướng dẫn đóng gói của Typer dùng uv để tạo khung package:

```bash
uv init --package kiem-tra
cd kiem-tra
```

Lệnh này tạo sẵn `pyproject.toml`, file `src/kiem_tra/__init__.py` có một hàm `main()` in lời chào, và một entry mặc định `kiem-tra = "kiem_tra:main"` trong `[project.scripts]`. Entry này sẽ được thay ở bước 3.

Kiểm tra: mở `pyproject.toml` và tìm bảng `[build-system]`. Python Packaging User Guide yêu cầu bảng này lúc nào cũng phải có, dù bạn dùng build backend nào.

Tiếp theo, chạy `uv add typer` để thêm Typer vào dependencies, rồi tạo file `src/kiem_tra/main.py`. Đoạn dưới là bản tối giản: khung lệnh đầy đủ, chỉ phần logic kiểm tra được để trống cho bạn điền vào.

```python
from pathlib import Path
import typer

app = typer.Typer()

@app.command()
def check(path: Path, max_rows: int = 1000):
    """Kiểm tra file CSV trước khi nạp vào pipeline."""
    ok = True  # thay bằng logic kiểm tra thật
    if not ok:
        raise typer.Exit(code=1)
    typer.echo(f"{path}: đạt")
```

Type hint `Path` và `int` cho Typer biết kiểu dữ liệu của từng tham số. Còn việc tham số nào bắt buộc thì theo đúng quy tắc của hàm Python: `path` không có giá trị mặc định nên phải truyền vào, còn `max_rows` có `= 1000` nên có thể bỏ qua. Hãy chạy `--help` để xem Typer hiển thị hai tham số này ra sao.

## Bước 3: khai báo lệnh, chỗ hay sai nhất

Theo Python Packaging User Guide, muốn package cài kèm một lệnh thì phải khai báo lệnh đó trong bảng `[project.scripts]`. Ví dụ của Typer có dạng `rick-portal-gun = "rick_portal_gun.main:app"`. Với package của bạn, sửa đúng dòng mặc định mà `uv init` đã tạo, đừng thêm dòng thứ hai trùng tên lệnh:

```toml
[project.scripts]
kiem-tra = "kiem_tra.main:app"
```

Vế trái là tên lệnh người dùng sẽ gõ, viết bằng gạch ngang. Vế phải là đường dẫn import, viết bằng gạch dưới, và phải trỏ tới đối tượng `app` chứ không trỏ tới hàm `check`.

Lý do nằm ở cách entry point hoạt động. Theo Packaging Guide, chạy lệnh này tương đương với việc import đối tượng được trỏ tới, gọi nó, rồi đưa kết quả trả về vào `sys.exit`.

Lời gọi đó không truyền tham số nào. Vì vậy, nếu bạn trỏ thẳng vào `check`, nhiều khả năng lệnh sẽ lỗi ngay khi chạy vì thiếu tham số, thay vì đi qua Typer để đọc dòng lệnh.

Cơ chế này còn cho thấy một điều: mã thoát của CLI là hợp đồng giữa bạn và hệ thống của khách. Cron job, CI hay script điều phối của họ sẽ đọc con số đó để quyết định có chạy tiếp hay không.

**Điểm mấu chốt:** Mã thoát của một công cụ dòng lệnh là lời hứa với mọi hệ thống gọi nó.

Kiểm tra: tạm đổi `ok = True` thành `ok = False` để giả lập file hỏng, rồi chạy qua `uv run` (lệnh này tự cài package vào môi trường của dự án trước khi chạy):

```bash
uv run kiem-tra --help
uv run kiem-tra du_lieu_hong.csv
echo $?   # mong đợi 1, không phải 0
```

## Bước 4–5: build wheel và giao theo cách khách không phải lo

```bash
uv build
```

Lệnh này tạo hai file trong thư mục `dist/`: một bản sdist `.tar.gz` và một file wheel `.whl`. File wheel là thứ bạn gửi cho khách hoặc đẩy lên kho package nội bộ của họ. Đến đây câu hỏi không còn là chuyện kỹ thuật nữa: máy của đội khách đang được thiết lập ra sao?

Câu hỏi đó quan trọng, vì lỗi ở đầu bài sinh ra từ việc cài chung vào Python của hệ thống.

Tài liệu uv giải thích rằng `uv tool install` cài mỗi công cụ vào một môi trường riêng, để phụ thuộc của tool, script và dự án không xung đột với nhau. pipx làm việc tương tự với các ứng dụng Python dành cho người dùng cuối: mỗi ứng dụng có virtualenv riêng và lệnh được đưa lên PATH.

Còn `uvx` chạy tool trong một môi trường tạm, cô lập, nên người dùng không phải cài trước. Với file wheel vừa build, hai cách gọi tương ứng là:

```bash
uv tool install ./dist/kiem_tra-0.1.0-py3-none-any.whl
uvx --from ./dist/kiem_tra-0.1.0-py3-none-any.whl kiem-tra du_lieu.csv
```

Ở phía khách, nên chốt một quy tắc: ai dùng lệnh mỗi ngày thì cài lâu dài, ai chỉ chạy thử thì dùng `uvx`. Ghi rõ cả hai cách trong README, mỗi cách một dòng lệnh. Đừng bắt họ đọc hướng dẫn cài Python.

## Những lỗi khiến công cụ chết sau khi bạn rời site

Lỗi đầu tiên là đặt tên lệnh quá chung chung, kiểu `check` hay `run`, rồi trùng với một lệnh có sẵn trên máy khách. Nên đặt tên gắn với nghiệp vụ, như `kiem-tra-don-hang`.

Lỗi tiếp theo là code lúc nào cũng thoát với mã 0, kể cả khi đã phát hiện dữ liệu hỏng. Người ngồi trước màn hình thì thấy dòng cảnh báo, nhưng pipeline của khách vẫn cứ thế nạp file hỏng.

Lỗi thứ ba là để khách cài wheel thẳng vào Python của hệ thống, đúng như tình huống đầu bài. Công cụ của bạn khi đó dùng chung thư viện với mọi thứ khác trên máy, và chỉ cần một lần nâng cấp là vỡ. README chỉ nên ghi lệnh pipx, `uv tool install` hoặc `uvx`, không ghi `pip install` trần.

Lỗi cuối cùng là chỉ test trên máy mình, nơi mọi phụ thuộc đã có sẵn. Trước khi gửi, hãy cài wheel vào một máy hoặc container sạch bằng đúng lệnh bạn ghi trong README.

## Kỹ năng này hiện ra thế nào trong công việc FDE?

Ở site khách, khoảng cách giữa "demo chạy được" và "khách tự vận hành được" thường nằm ở những việc nhỏ như thế này. Một CLI có `--help` rõ ràng, có auto-completion và trả mã thoát đúng sẽ giảm hẳn số tin nhắn hỏi "chạy thế nào" sau khi bạn rời đi. Khách cũng đưa được nó vào cron hay CI mà không cần gọi bạn.

Khi đọc mô tả công việc FDE, hãy để ý những yêu cầu về công cụ nội bộ hay bàn giao cho đội khách tự vận hành: đó là chỗ kỹ năng này được dùng.

Trong CV, đừng viết "biết Python packaging". Hãy viết rằng bạn đã đóng gói một công cụ kiểm tra dữ liệu thành CLI để đội vận hành của khách tự chạy hằng ngày, và nói rõ ai đã dùng nó.

Script nên đóng gói đầu tiên là script mà tuần này bạn phải giải thích cách chạy nhiều lần nhất.

**Thử ngay tuần này:**

- Lấy một script bạn hay gửi qua chat cho khách, đóng gói lại theo năm bước trong bài và tự cài thử bằng uv tool install trên một máy sạch.
- Viết README dài đúng một màn hình, gồm lệnh cài, một lệnh ví dụ và ý nghĩa của mã thoát, rồi nhờ một người không biết code làm theo.
- Thêm vào CV một dòng mô tả công cụ nội bộ bạn đã bàn giao, kèm số người hoặc số đội đã tự dùng nó.

## Nguồn

- [Typer](https://typer.tiangolo.com/)

- [Building a Package - Typer](https://typer.tiangolo.com/tutorial/package/)

- [Writing your pyproject.toml](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/)

- [pipx](https://pipx.pypa.io/stable/)

- [Using tools](https://docs.astral.sh/uv/guides/tools/)
