---
jupyter:
  kernelspec:
    display_name: Python 3 (ipykernel)
    language: python
    name: python3
  language_info:
    codemirror_mode:
      name: ipython
      version: 3
    file_extension: .py
    mimetype: text/x-python
    name: python
    nbconvert_exporter: python
    pygments_lexer: ipython3
    version: 3.11.9
  nbformat: 4
  nbformat_minor: 5
---
# Quản lý Môi trường Python

Trong các dự án Python hiện đại, việc quản lý môi trường và tự động hóa tác vụ trở nên dễ dàng hơn nhờ công cụ **uv**(trình quản lý gói & môi trường nhanh), kết hợp với **Poe the Poet** (trình chạy tác vụ), bên cạnh công cụ quen thuộc là **pip**. Tài liệu này hướng dẫn chi tiết cách sử dụng mẫu `pyproject.toml` chuẩn để thiết lập dependencies, cấu hình lint/test, quản lý môi trường (phát triển & sản xuất) trên cả máy online lẫn offline, cũng như quy trình làm việc với Git khi dùng `uv`.

## Template `pyproject.toml` 

Dưới đây là mẫu nội dung tệp **pyproject.toml** cho một dự án Python sử dụng `uv`. Mỗi phần đều có chú thích giải thích mục đích:

```toml
# [project]: Thông tin dự án và khai báo các phụ thuộc chính
[project]
name = "my-awesome-project"            # Tên dự án
version = "0.1.0"                      # Phiên bản dự án
description = "Mô tả ngắn gọn dự án"   # Mô tả dự án
readme = "README.md"                   # Tệp README đi kèm dự án
requires-python = ">=3.10"             # Yêu cầu phiên bản Python

# Danh sách dependencies chính (sẽ cài cho cả môi trường dev và prod)
dependencies = [
    "fastapi>=0.110.0",        # Web framework FastAPI (yêu cầu phiên bản >= 0.110.0)
    "uvicorn[standard]",       # Server chạy FastAPI (cài cả extra [standard])
    "sqlalchemy>=1.4",         # ORM cho cơ sở dữ liệu
]

# Các thư viện chỉ dùng cho môi trường phát triển (development)
# Ưu tiên sử dụng [dependency-groups] thay cho [project.optional-dependencies]
# Chạy `uv lock` hay `uv sync` mặc định bao gồm [dependency-groups]
# Chạy cho môi trường production:
#    1.loại bỏ nhóm `dev` với flag `--no-dev` (alias của `--no-group dev`)
#    2.Tắt toàn bộ default groups với `--no-default-groups`
#    3.Nếu bạn đã định nghĩa thêm nhóm `prod` trong `[dependency-groups]`, chỉ cần kết hợp `--no-default-groups --group prod` để cài riêng production
[dependency-groups]
dev = [
    "pytest",
    "pytest-cov",
    "ruff",
    "mypy",
    "jupyterlab",
    "notebook",
    "ipykernel",
    "seaborn",
    "ipywidgets",
    "ydata-profiling",
    # ... có thể bổ sung các công cụ khác (ví dụ: coverage, flake8, v.v.)
]

# [project.optional-dependencies]: Nhóm các phụ thuộc tùy chọn (extras), tuy nhiên ưu tiên sử dụng cơ chế [dependency-groups]
# Định nghĩa nhóm "dev" cho môi trường phát triển, gồm các công cụ phục vụ dev/test, chạy thông qua --extra dev
# [project.optional-dependencies]
# dev = [
#     "pytest>=7.0",       # Thư viện test
#     "ruff>=0.0.272",     # Linter kiểm tra code tự động
#     "black>=23.3",       # Formatter định dạng code
#     "mypy>=0.950",       # Trình kiểm tra kiểu (type checking)
#     "poethepoet>=0.13",  # Task runner Poe the Poet
#     # ... có thể bổ sung các công cụ khác (ví dụ: coverage, flake8, v.v.)
# ]

# Cấu hình cho uv
[tool.uv]
# Khai báo default-groups bao gồm những môi trường gì, mặc định đã là `default-groups = ["dev"]` mà không cần khai báo
default-groups = ["dev"]
# Để chạy cho production, chỉ cần `uv lock --no-dev` ; `uv sync --no-dev`; `uv sync --no-default-groups`
#--no-default-groups: tắt toàn bộ nhóm mặc định (là dev theo cấu hình ở trên).
#--no-dev: tắt riêng nhóm dev (alias của --no-group dev).

# Cấu hình cho công cụ linting/formatting (ruff, black) và testing (pytest)
[tool.black]
line-length = 88        # Độ dài dòng tối đa theo chuẩn PEP8
target-version = ["py310"]  # Phiên bản Python mục tiêu

[tool.ruff]
line-length = 88
select = ["E", "F", "W", "C90"]  # Quy tắc lint cần check (ví dụ E,F,W của pyflake8, C90 cho black)
ignore = []                     # Có thể liệt kê mã lỗi muốn bỏ qua tại đây
extend-exclude = ["node_modules", "venv"]  # Bỏ qua các thư mục không cần lint

[tool.pytest.ini_options]
# Cấu hình mặc định cho pytest (tương đương nội dung pytest.ini)
minversion = "7.0"
addopts = "-ra -q"        # Tham số mặc định khi chạy pytest (-ra -q: báo cáo rút gọn)
testpaths = ["tests"]     # Thư mục chứa mã nguồn các bài test

# Cấu hình các task cho Poe the Poet
[tool.poe.tasks]
# Task chạy bộ kiểm thử
test = "pytest tests/"

# Task kiểm tra code với ruff
lint = "ruff check ."

# Task tự động định dạng code với black
format = "black ."

# Task dọn dẹp file tạm, cache, build
clean = { shell = "rm -rf __pycache__ .pytest_cache build dist *.py[cod]" }

# Task thiết lập môi trường (cài package, cài pre-commit hook chẳng hạn)
setup = { shell = """
uv sync --extra dev
pre-commit install
""" }

```



_Mô tả:_ 
- Phần `[project]` liệt kê thông tin dự án và **phụ thuộc chính** (những package cần cho runtime chính của ứng dụng).
- Phần `[project.optional-dependencies]` định nghĩa nhóm `dev` cho **phụ thuộc phát triển** – những thư viện chỉ dùng khi phát triển, testing, không cần thiết trong môi trường chạy thực tế (prod).
- Bên dưới, các phần `[tool.black]`, `[tool.ruff]`, `[tool.pytest.ini_options]` cấu hình cho formatter, linter, và test runner (giúp thống nhất style code và hành vi test trong dự án).
- Cuối cùng, `[tool.poe.tasks]` khai báo các **tác vụ (tasks)** thường dùng, như chạy test, lint, format code, dọn dẹp, v.v., để có thể gọi nhanh bằng lệnh `poe`. (Chi tiết về cách dùng Poe được trình bày ở phần sau.)

## Setup Environments

Khi làm việc với `uv`, chúng ta tuân theo nguyên tắc: khai báo dependencies trong `pyproject.toml`, **lock** chúng lại thành phiên bản cụ thể trong `uv.lock`, sau đó **sync** vào môi trường ảo. Trước tiên, cần hiểu khái niệm **lock** và **sync**:

- **Lock**: tiến trình giải quyết phiên bản phù hợp cho các phụ thuộc trong `pyproject.toml` và tạo ra file **uv.lock**chứa **đúng phiên bản** và cây phụ thuộc đầy đủ của chúng. Nói cách khác, `uv.lock`là nguồn chân lý về môi trường đã được “đóng băng” (freeze).

- **Sync**: tiến trình cài đặt các gói vào môi trường ảo sao cho **khớp với lockfile**. Lệnh `uv sync` sẽ cài đặt _đúng các phiên bản_ đã khóa trong `uv.lock` và **gỡ bỏ** những gói không có trong lockfile (theo mặc định, chế độ sync là “exact”)


Quy trình tổng quát để thiết lập môi trường là:
- **(1)** tạo môi trường ảo (venv)
- **(2)** tạo/ cập nhật lockfile lock
- **(3)** cài đặt đồng bộ sync

Dưới đây là hướng dẫn cụ thể cho các trường hợp:

### Máy **Online** (có kết nối Internet)

#### Dev environment

(Môi trường bao gồm cả phụ thuộc chính và phụ thuộc dev)

1. **Tạo môi trường ảo:** Sử dụng `uv` để tạo nhanh venv. Chỉ cần chạy:

```bash
uv venv .venv
```

- Lệnh này tạo thư mục `.venv` và cài sẵn `pip`/`setuptools` bên trong.
- Có thể chỉ định phiên bản Python bằng `uv venv --python <phiên bản>` nếu cần dùng bản khác.

2. **Cài đặt dependencies vào môi trường ảo:** Do máy có mạng, ta có thể cài trực tiếp từ PyPI. Có hai cách:

    - **Cách 1: Cài từ `pyproject.toml`** – Dùng `uv` để lock và sync tự động:
		-  Nếu trong `pyproject.toml` chứa `[project.optional-dependencies] dev = [...]`:
	        1. Giải quyết phụ thuộc kể cả nhóm [dev], tạo `uv.lock`:  `uv lock --extra dev
	        2. Tạo .venv (nếu chưa có) và cài đúng phiên bản gói trong `uv.lock`: `uv sync --extra dev`
		- Nếu trong `pyproject.toml` chứa `[dependency-groups] dev = [...]`:
			1. `uv lock `
			2. `uv sync`
			Có thể khai báo thêm trong `pyproject.toml` là:
			```toml
			[tool.uv]
			# Để mọi lệnh uv sync và uv lock tự động bao gồm cả project dependencies (main) và nhóm dev
			default-groups = ["dev"]
			```
			để mặc định sử dụng `uv lock` và `uv sync` sẽ bao gồm nhóm mặc định (ví dụ `dev`), còn nếu trong production sẽ chỉ cần `uv lock --no-dev` ; `uv sync --no-dev`; `uv sync --no-default-groups`

        Tham số `--extra dev` đảm bảo nhóm phụ thuộc optional "dev" cũng được bao gồm khi lock và sync (ở đây do ta khai báo dev trong optional-dependencies). Sau lệnh này, mọi gói (kể cả pytest, black, v.v.) sẽ được cài vào `.venv`.

        Lưu ý: Nếu sử dụng **dependency group** đặc biệt của uv thay cho optional extras:
		- ví dụ `uv add --dev .` – thì uv mặc định **tự động bao gồm** nhóm dev khi sync mà không cần chỉ rõ. Trường hợp đó có thể chỉ cần `uv lock` rồi `uv sync` là đã gồm phụ thuộc dev. Còn để bỏ qua nhóm dev, dùng cờ `--no-dev`

    - **Cách 2: Sử dụng file yêu cầu (requirements)** – Phương pháp truyền thống tương tự pip:

        1. Trước tiên, tạo file `requirements.txt` từ `pyproject.toml`: `uv pip compile pyproject.toml --extra dev -o requirements.txt`

            Lệnh trên sẽ resolve dependencies (bao gồm nhóm dev) và xuất danh sách phiên bản cụ thể vào `requirements.txt` (tương tự pip-compile của pip-tools)

        2. Sau đó, cài đặt môi trường theo file này: `uv pip sync requirements.txt`

            (Hoặc có thể dùng `pip install -r requirements.txt` bên trong `.venv` đều được). Cách này hữu ích nếu muốn giữ lại `requirements.txt` để dễ so sánh phiên bản, nhưng với `uv` thì việc dùng thẳng `uv.lock` là tối ưu hơn.


#### Prod environment

Chỉ bao gồm các phụ thuộc chính, không gồm phụ thuộc `dev`. Thông thường, ta sẽ tạo lockfile chỉ với dependencies chính và cài đặt tương ứng:

- Nếu dùng phương pháp optional extras như trên, chỉ cần bỏ tham số `--extra dev`. Ví dụ:
    - lock chỉ bao gồm dependencies chính: `uv lock`
    - cài đặt môi trường chỉ với dependencies chính: `uv sync`

    Lúc này các gói dev (pytest, black,...) sẽ không có trong `uv.lock` và không được cài đặt.

- Nếu dùng dependency groups của uv (ví dụ có nhóm `dev` riêng), có thể dùng:

    - loại bỏ gói dev khi cài đặt: `uv sync --no-dev`
    - hoặc chỉ định cụ thể nhóm cần cài bằng `uv sync --group prod --no-group dev` nếu bạn có nhóm `prod` riêng cho các phụ thuộc chỉ dùng ở production.


**Kiểm tra:** Sau khi sync, có thể kích hoạt venv (`source .venv/bin/activate` trên Linux/Mac, hoặc `.venv\Scripts\activate.bat` trên Windows) và chạy thử ứng dụng hoặc chạy `pip list` để kiểm tra các gói đã cài.

### Máy **Offline** (không có kết nối Internet)

Thiết lập môi trường trên máy không có internet (ví dụ máy chủ nội bộ, môi trường tách biệt) đòi hỏi chuẩn bị trước các gói cần thiết. Giả sử ta có một máy **online** (máy cá nhân) và một máy **offline** (máy đích triển khai hoặc phát triển trong mạng kín). Quy trình sẽ như sau:

**Trên máy Online (chuẩn bị):**

1. **Chuẩn bị code và lockfile:** Đảm bảo `pyproject.toml` đã khai báo đúng các phụ thuộc cần thiết. Chạy `uv lock --extra dev` (hoặc cách tương tự) trên máy online để tạo/ cập nhật `uv.lock`. Commit và đẩy (push) cả `pyproject.toml` và `uv.lock` lên repository Git (VD: GitLab công ty) để máy offline có thể lấy về sau.

2. **Tải trước các gói (download wheels):** Dùng máy online có internet để tải toàn bộ gói phụ thuộc về dạng file nén (.whl) lưu trữ cục bộ:

    - Tạo thư mục để chứa các file wheel, ví dụ: `mkdir offline_packages`
    - Dùng `uv` hoặc `pip` để tải tất cả gói trong danh sách yêu cầu về thư mục trên: `uv pip download -r requirements-dev.txt --dest offline_packages`
    - Hoặc dùng pip: `pip download -r requirements-dev.txt --dest offline_packages`

		Lệnh trên sẽ tải về toàn bộ file `.whl` của các package (kể cả phụ thuộc con) vào thư mục `offline_packages`. Bạn có thể dùng `requirements.txt` hoặc `requirements-dev.txt` tùy trường hợp (nếu muốn bao gồm phụ thuộc dev). Kết quả, thư mục `offline_packages` sẽ chứa tất cả file cần thiết để cài đặt sau này.

    - **Lưu ý:** Đảm bảo phiên bản Python trên máy offline tương thích với các wheel đã tải (nên dùng cùng phiên bản major, ví dụ đều là Python 3.10 hoặc 3.11, để tránh xung đột)

3. **Chuẩn bị công cụ `uv`:** Nếu máy offline chưa cài `uv`, hãy tải sẵn binary của `uv` tương ứng hệ điều hành:

    - Truy cập trang release của dự án uv trên GitHub và tải file phát hành phù hợp.
    - Giải nén lấy tệp thực thi `uv` để sẵn sàng chuyển sang máy offline.

4. **Chuyển dữ liệu sang máy Offline:** Sử dụng ổ cứng di động hoặc kênh truyền file an toàn để chuyển:
	1. **(a)** thư mục `offline_packages` chứa các file .whl, 
	2. **(b)** file thực thi `uv` (nếu cần)
	3. **(c)** mã nguồn dự án (bao gồm `pyproject.toml` và `uv.lock`) sang máy offline


**Trên máy Offline (cài đặt):**

1. **Thiết lập dự án:** Clone hoặc cập nhật code dự án từ repository (qua hình thức cho phép, ví dụ GitLab nội bộ hoặc chép tay). Đảm bảo thư mục dự án trên máy offline có đủ `pyproject.toml` và `uv.lock` (đã được chuẩn bị ở bước trước).

2. **Cài đặt `uv`:** Nếu máy offline chưa có `uv`, đặt file thực thi `uv` đã chuyển vào một thư mục (vd. thư mục dự án) và cấp quyền thực thi (`chmod +x uv` trên Linux). Có thể đặt alias hoặc thêm vào PATH để gọi `uv` thuận tiện.

3. **Tạo môi trường ảo:** Vào thư mục gốc dự án trên máy offline và tạo venv:
	1. Tạo .venv bằng uv (dùng ./uv nếu uv chưa trong PATH): `./uv venv .venv`
	2. Kích hoạt (Linux/Mac) `source .venv/bin/activate` hoặc (Windows) `.venv\Scripts\activate.bat`

4. **Cài đặt gói từ nguồn offline:** Sử dụng lệnh `uv pip sync` kết hợp tham số `--find-links` trỏ đến thư mục chứa các file wheel:

    `./uv pip sync --find-links ../offline_packages requirements.txt`

    - Lệnh trên yêu cầu uv/pip cài đặt theo `requirements.txt` nhưng **không tải từ PyPI**, thay vào đó tìm các gói cần thiết trong thư mục `offline_packages` đã cung cấp. Đảm bảo đường dẫn `offline_packages` chính xác (trong ví dụ trên, giả sử `offline_packages` nằm cùng cấp thư mục dự án; nếu ở vị trí khác thì chỉnh lại đường dẫn đầy đủ).
    - Có thể dùng lệnh thuần pip tương đương: `pip install -r requirements-dev.txt --no-index --find-links offline_packages` nếu cần

    Sau khi chạy, các gói trong `.venv` sẽ giống hệt môi trường trên máy online. Bạn có thể kích hoạt venv và kiểm tra bằng `pip list`.

Máy offline giờ đã có môi trường ảo với đầy đủ thư viện cần thiết mà không cần internet. Mỗi khi có thay đổi dependencies, bạn lặp lại quy trình: cập nhật `pyproject.toml` + lockfile trên máy online, tải thêm wheel mới, rồi chuyển qua và chạy lại `uv pip sync --find-links ...` trên máy offline để cập nhật môi trường.

## `poe` thiết lập tasks

Xem template `pyproject.toml` sử dụng poe để thiết lập task, khi đó chỉ cần chạy command: `poe <task_name>` để run scripts

Kết hợp `uv` và **Poe the Poet** giúp tự động hóa nhiều tác vụ thường gặp trong quá trình phát triển:

- **Dựng môi trường ảo (`venv`):** Như đã nói, dùng `uv venv` để tạo môi trường nhanh chóng.
	- Nếu lỡ xóa `.venv`, chỉ cần chạy lại `uv sync` (hoặc thậm chí `uv run` khi chạy một lệnh bất kỳ) – `uv` sẽ tự phát hiện thiếu venv và tạo lại, cài đúng gói theo lockfile.

- **Cài đặt dependencies:** Sau khi thay đổi `pyproject.toml` (hoặc thêm package), hãy chạy `uv lock` rồi `uv sync`để áp dụng.
	- Nếu dùng lệnh tiện ích `uv add` (xem phần Git workflow bên dưới), `uv` sẽ tự động cập nhật cả `pyproject.toml` và `uv.lock` cho bạn. Khi đó chỉ cần `uv sync` là môi trường được cập nhật. Mặc định, `uv sync`sẽ cài tất cả gói chính và gói dev (nếu dev được khai báo dạng dependency group). Bạn có thể loại trừ gói dev bằng `--no-dev` khi chạy sync nếu muốn chỉ cài gói cho production.

- **Chạy test (`pytest`):** Với task `test` đã định nghĩa trong `pyproject.toml`, bạn có thể thực thi bằng cách gõ:

    `uv run poe test`

    - Lệnh `uv run poe <task>` sẽ kích hoạt môi trường `.venv` và chạy task thông qua Poe the Poet. Task `test` ở trên tương đương chạy `pytest tests/` toàn bộ testcases.

- **Lint và Format code:** Tương tự, dùng:

    `uv run poe lint    # chạy ruff để kiểm tra code uv run poe format  # chạy black (hoặc ruff --fix) để định dạng lại code`

    - Bạn có thể kết hợp nhiều task một lúc, ví dụ:

    `uv run poe format lint test`

    - Sẽ tự động format code, rồi lint kiểm tra, xong chạy tests liên tục. Nhờ Poe the Poet, các lệnh dài dòng được cấu hình sẵn, giúp mọi người trong nhóm chạy chúng thống nhất mà không phải nhớ câu lệnh phức tạp.

- **Clean (dọn dẹp):** Task `clean` trong ví dụ trên xóa các file/thư mục tạm (`__pycache__`, thư mục build, dist, cache pytest...). Chạy:

    `uv run poe clean`

    - để làm sạch dự án, đặc biệt hữu ích trước khi đóng gói hay khi reset môi trường.


_Ngoài ra:_ Bạn có thể định nghĩa thêm nhiều task tùy nhu cầu (ví dụ `setup` để cài pre-commit hooks như trong mẫu). Poe the Poet hỗ trợ cả các task phức tạp với script nhiều dòng, tham số truyền vào. Hãy tham khảo tài liệu Poe để tận dụng tối đa khả năng tự động hóa.

## Git Workflow using `uv`

Khi làm việc nhóm với repository Git, tuân thủ một số quy tắc sẽ giúp đồng bộ môi trường giữa các thành viên và CI/CD:

### Thiết lập dự án khi mới clone

Khi bạn **clone** một dự án Python dùng `uv` về máy local, việc đầu tiên là tái tạo môi trường giống như người tạo dự án. Giả sử repo đã có sẵn `pyproject.toml` và `uv.lock` được commit bởi người phát triển trước:

1. **Tạo môi trường ảo và cài đặt nhanh:** Chỉ cần chạy lệnh:

    `uv sync    # Tự động tạo .venv (nếu chưa có) và cài đặt theo uv.lock`

    - Lệnh này đọc các phụ thuộc khai báo trong `pyproject.toml` và phiên bản cụ thể trong `uv.lock`, sau đó tạo môi trường ảo và cài đúng các gói đã khóa phiên bản. Môi trường của bạn sẽ **đồng bộ và reproducible** so với máy của người tạo. (Thực tế, bạn cũng có thể chạy `uv run poe setup` nếu có task setup; `uv` sẽ tự động `lock` và `sync` trước khi chạy task).

2. **Kích hoạt môi trường và sử dụng:** Kích hoạt venv (`source .venv/bin/activate` trên Linux/Mac, hoặc `.\.venv\Scripts\activate` trên Windows). Giờ bạn có thể bắt đầu chạy ứng dụng hoặc các lệnh dev (như `uv run poe lint/test`) trong môi trường này.


### Thêm hoặc thay đổi phụ thuộc mới

Giả sử bạn cần cài đặt thêm một thư viện mới (vd: `httpx`) hoặc cập nhật phiên bản thư viện trong dự án:

1. **Cập nhật khai báo phụ thuộc:** Bạn có thể chỉnh sửa trực tiếp file `pyproject.toml` (thêm `httpx` vào `[project.dependencies]` hoặc tương ứng) _hoặc_ dùng lệnh tiện lợi:

    `uv add httpx`

    - Lệnh `uv add` sẽ thêm gói vào pyproject và tự động xử lý phiên bản phù hợp.
    - Thêm tùy chọn `--dev` nếu đó là phụ thuộc phục vụ phát triển (sẽ thêm vào nhóm dev)
    - `uv` sẽ cập nhật `pyproject.toml` và tiến hành resolve tạo/điều chỉnh `uv.lock` ngay khi thêm (tương tự cách Poetry hoạt động). Kết quả là file lock được làm mới với phiên bản cố định của thư viện mới.

2. **Đồng bộ môi trường local:** Chạy lệnh:

    `uv sync`

    - để cài đặt thư viện mới vào `.venv` của bạn. Nếu thư viện có phụ thuộc con, `uv` cũng sẽ cài đầy đủ theo `uv.lock`. Sau bước này, dự án của bạn đã bao gồm `httpx` sẵn sàng để sử dụng.

3. **Commit thay đổi:** 
	- **Rất quan trọng:** hãy commit **cả hai file** `pyproject.toml` (nơi thể hiện ý định thêm phụ thuộc) **và** `uv.lock` (nơi thể hiện kết quả phiên bản cụ thể). Ví dụ:
	```bash
	git add pyproject.toml uv.lock
	git commit -m "feat: Thêm thư viện httpx để gọi API"
	git push
	```

    - Việc commit lockfile đảm bảo các thành viên khác và hệ thống CI/CD đều cài đúng phiên bản như bạn, giúp môi trường đồng nhất.

4. **Xử lý khi có thay đổi lớn:** 
	1. Nếu bạn cập nhật nhiều phụ thuộc hoặc thay đổi phiên bản Python, hãy ghi chú trong README và đảm bảo test kỹ sau khi `uv lock` mới.
	2. Trong một số trường hợp, `uv.lock` có thể **xung đột** khi merge Git (ví dụ hai người chỉnh dependencies khác nhau trên hai nhánh).
		1. Khi đó, cách đơn giản là chọn giữ lại `pyproject.toml` đúng theo ý muốn, rồi chạy lại `uv lock` trên bản hợp nhất để sinh ra `uv.lock` mới thống nhất,
		2. sau đó commit lại. Luôn luôn `git pull` trước khi `git push` để giảm nguy cơ conflict.


### Cập nhật môi trường khi kéo code về

Khi đồng đội của bạn đã thêm/đổi phụ thuộc và đẩy lên repository (cập nhật `pyproject.toml` và `uv.lock`), bạn cần cập nhật môi trường local của mình:

1. **Git pull:** Kéo những thay đổi mới nhất: `git pull`

    - Sau đó kiểm tra log hoặc diff, bạn sẽ thấy `pyproject.toml` và `uv.lock` có thể đã thay đổi (ví dụ thêm thư viện mới).

2. **Đồng bộ môi trường:** Chạy lại: `uv sync`

    - `uv` sẽ đọc file lock mới, nhận ra những gói nào chưa có trong môi trường của bạn và cài bổ sung, đồng thời gỡ bỏ gói thừa nếu có để khớp với lockfile.
    - Sau bước này, môi trường local của bạn sẽ giống hệt người đẩy code. Bạn có thể tiếp tục làm việc mà không lo lỗi thiếu thư viện.

3. **Thực thi kiểm tra nhanh:** Nên chạy lại `uv run poe lint` và `uv run poe test` để đảm bảo mọi thứ vẫn ổn định sau khi cập nhật phụ thuộc.


### Thói quen làm việc hàng ngày

- **Luôn đồng bộ trước khi làm:** Mỗi ngày hoặc mỗi khi bắt đầu phiên làm việc, hãy `git pull` để lấy code mới nhất về, sau đó `uv sync` để chắc chắn môi trường đã bao gồm mọi thay đổi từ người khác. Điều này giúp tránh lỗi do thiếu thư viện hoặc phiên bản không khớp.

- **Luôn đồng bộ trước khi đẩy:** Trước khi `git push`, nếu bạn đã cài thêm gói mới mà quên cập nhật `pyproject.toml`, hãy bổ sung ngay (bằng `uv add` hoặc chỉnh tay rồi `uv lock`). Đảm bảo _nguồn chân lý_`pyproject.toml` và _kết quả_ `uv.lock` của bạn phản ánh đúng môi trường đang chạy. Sau đó mới commit và push.

- **Không chỉnh sửa thủ công `uv.lock`:** Chỉ cập nhật `uv.lock` thông qua lệnh `uv` (như `uv lock`, `uv add`, `uv remove`). Tránh sửa tay file này để phòng sai lệch định dạng hoặc thiếu sót phiên bản.

- **Quản lý phiên bản Python:** Nếu dự án cố định phiên bản Python (ví dụ 3.10), nên ghi trong file `.python-version` và README để mọi người dùng đúng phiên bản. `uv` không bắt buộc nhưng điều này giúp tránh khác biệt môi trường.

```.python-version
3.12.0
```

- **Cập nhật `uv`:** Phiên bản `uv` nên được cài giống nhau trên các máy (dù uv có tính tương thích ngược khá tốt). Có thể ghi chú phiên bản uv khuyên dùng trong README.

## Quy trình làm việc với Offline Machine use `UV`

**Tóm tắt**
Quy trình này đảm bảo bạn có thể chuẩn bị một “cache” đầy đủ trên máy **online** rồi mang sang máy **offline**, để uv tái tạo môi trường với cùng phiên bản Python, pip, uv, wheel, và tất cả packages. Trên máy online, bạn sẽ:

1. **Pin** phiên bản Python, uv, pip.
2. Chạy `uv lock` để khoá dependencies với URL chính xác.
3. Chạy `uv sync` (với `--cache-dir` tuỳ chọn) để tải về và lưu toàn bộ packages vào cache [docs.astral.sh](https://docs.astral.sh/uv/reference/cli/?utm_source=chatgpt.com).

Sau đó, mang cả **project** và **thư mục cache** sang máy offline, thiết lập `UV_OFFLINE=1` và `UV_CACHE_DIR` trỏ tới cache đó [build.opensuse.org](https://build.opensuse.org/projects/system%3Ahomeautomation%3Ahome-assistant/packages/python-uv/files/python-uv.changes?expand=0&utm_source=chatgpt.com), rồi chạy `uv sync --offline` để cài đặt mà không cần mạng, đảm bảo 100% đồng nhất.

### 1. Trên máy Online

#### Tạo `pyproject.toml`

```toml
# [project]: Thông tin dự án và khai báo các phụ thuộc chính
[project]
name = "my-awesome-project"            # Tên dự án
version = "0.1.0"                      # Phiên bản dự án
description = "Mô tả ngắn gọn dự án"   # Mô tả dự án
readme = "README.md"                   # Tệp README đi kèm dự án
requires-python = ">=3.10"             # Yêu cầu phiên bản Python

# Danh sách dependencies chính (sẽ cài cho cả môi trường dev và prod)
dependencies = [
    "fastapi>=0.110.0",        # Web framework FastAPI (yêu cầu phiên bản >= 0.110.0)
    "uvicorn[standard]",       # Server chạy FastAPI (cài cả extra [standard])
    "sqlalchemy>=1.4",         # ORM cho cơ sở dữ liệu
]

# Các thư viện chỉ dùng cho môi trường phát triển (development)
# Ưu tiên sử dụng [dependency-groups] thay cho [project.optional-dependencies]
# Chạy `uv lock` hay `uv sync` mặc định bao gồm [dependency-groups]
# Chạy cho môi trường production:
#    1.loại bỏ nhóm `dev` với flag `--no-dev` (alias của `--no-group dev`)
#    2.Tắt toàn bộ default groups với `--no-default-groups`
#    3.Nếu bạn đã định nghĩa thêm nhóm `prod` trong `[dependency-groups]`, chỉ cần kết hợp `--no-default-groups --group prod` để cài riêng production
[dependency-groups]
dev = [
    "pytest",
    "pytest-cov",
    "ruff",
    "mypy",
    "jupyterlab",
    "notebook",
    "ipykernel",
    "seaborn",
    "ipywidgets",
    "ydata-profiling",
    # ... có thể bổ sung các công cụ khác (ví dụ: coverage, flake8, v.v.)
]

# [project.optional-dependencies]: Nhóm các phụ thuộc tùy chọn (extras), tuy nhiên ưu tiên sử dụng cơ chế [dependency-groups]
# Định nghĩa nhóm "dev" cho môi trường phát triển, gồm các công cụ phục vụ dev/test, chạy thông qua --extra dev
# [project.optional-dependencies]
# dev = [
#     "pytest>=7.0",       # Thư viện test
#     "ruff>=0.0.272",     # Linter kiểm tra code tự động
#     "black>=23.3",       # Formatter định dạng code
#     "mypy>=0.950",       # Trình kiểm tra kiểu (type checking)
#     "poethepoet>=0.13",  # Task runner Poe the Poet
#     # ... có thể bổ sung các công cụ khác (ví dụ: coverage, flake8, v.v.)
# ]

# Cấu hình cho uv
[tool.uv]
# Khai báo default-groups bao gồm những môi trường gì, mặc định đã là `default-groups = ["dev"]` mà không cần khai báo
default-groups = ["dev"]
# Để chạy cho production, chỉ cần `uv lock --no-dev` ; `uv sync --no-dev`; `uv sync --no-default-groups`
#--no-default-groups: tắt toàn bộ nhóm mặc định (là dev theo cấu hình ở trên).
#--no-dev: tắt riêng nhóm dev (alias của --no-group dev).

# Cấu hình cho công cụ linting/formatting (ruff, black) và testing (pytest)
[tool.black]
line-length = 88        # Độ dài dòng tối đa theo chuẩn PEP8
target-version = ["py310"]  # Phiên bản Python mục tiêu

[tool.ruff]
line-length = 88
select = ["E", "F", "W", "C90"]  # Quy tắc lint cần check (ví dụ E,F,W của pyflake8, C90 cho black)
ignore = []                     # Có thể liệt kê mã lỗi muốn bỏ qua tại đây
extend-exclude = ["node_modules", "venv"]  # Bỏ qua các thư mục không cần lint

[tool.pytest.ini_options]
# Cấu hình mặc định cho pytest (tương đương nội dung pytest.ini)
minversion = "7.0"
addopts = "-ra -q"        # Tham số mặc định khi chạy pytest (-ra -q: báo cáo rút gọn)
testpaths = ["tests"]     # Thư mục chứa mã nguồn các bài test

# Cấu hình các task cho Poe the Poet
[tool.poe.tasks]
# Task chạy bộ kiểm thử
test = "pytest tests/"

# Task kiểm tra code với ruff
lint = "ruff check ."

# Task tự động định dạng code với black
format = "black ."

# Task dọn dẹp file tạm, cache, build
clean = { shell = "rm -rf __pycache__ .pytest_cache build dist *.py[cod]" }

# Task thiết lập môi trường (cài package, cài pre-commit hook chẳng hạn)
setup = { shell = """
uv sync --extra dev
pre-commit install
""" }

```



_Mô tả:_ 
- Phần `[project]` liệt kê thông tin dự án và **phụ thuộc chính** (những package cần cho runtime chính của ứng dụng).
- Bên dưới, các phần `[tool.black]`, `[tool.ruff]`, `[tool.pytest.ini_options]` cấu hình cho formatter, linter, và test runner (giúp thống nhất style code và hành vi test trong dự án).
- Cuối cùng, `[tool.poe.tasks]` khai báo các **tác vụ (tasks)** thường dùng, như chạy test, lint, format code, dọn dẹp, v.v., để có thể gọi nhanh bằng lệnh `poe`. (Chi tiết về cách dùng Poe được trình bày ở phần sau.)

#### Pin Python, uv và pip

##### Cài và ghi nhận chính xác phiên bản Python

- Cách 1: Cài phiên bản python: `uv python install 3.12.0`
- Cách 2: Tạo file `.python-version` khai báo chính xác python version
```.python-version
3.12.0
```

##### Cập nhật uv và pip trong môi trường đó
```bash
uv self update
pip install --upgrade pip
```

#### Tạo lockfile với URL chính xác

Lockfile (`uv.lock`) sẽ chứa các đường dẫn tải về (wheel, sdist) chính xác, không cần truy vấn lại index khi offline.
```bash
# mặc định sẽ bao gồm thêm cả các default-groups (giá trị mặc định của default-groups là ["dev"])
uv lock
```

#### Sync và tạo cache

1. Xác định hoặc tuỳ chỉnh nơi lưu cache và chạy `uv sync`:
	```bash
	UV_CACHE_DIR=./offline-cache
    uv sync
	```

2. Hoặc chạy trực tiếp với tham số
	```
	uv sync --cache-dir=./offline-cache
	```
uv sẽ tải toàn bộ wheels, sdists, Git archives… và lưu vào `./offline-cache` (hoặc `$XDG_CACHE_HOME/uv`) [docs.astral.sh](https://docs.astral.sh/uv/reference/cli/?utm_source=chatgpt.com).

3. (Tuỳ chọn) Kiểm tra/thu dọn cache:
```bash
uv cache dir     # xem đường dẫn cache
uv cache prune   # loại bỏ cache không dùng
uv cache clean   # xóa toàn bộ cache
```

#### Đóng gói cache

Sau khi `uv sync` hoàn tất, đóng gói thư mục `offline-cache/` (ví dụ tarball hoặc zip) để chuyển sang máy offline.

### 2. Trên máy Offline

#### Clone source code
```bash
git clone https://gitlab.internal/your-project.git
cd your-project
```
Đảm bảo dự án có `pyproject.toml` và `uv.lock` đã commit.

#### Triển khai cache

Giải nén hoặc đặt thư mục cache (ví dụ `/opt/uv-cache`) lên máy offline.

#### Cấu hình uv offline

Thiết lập biến môi trường hoặc flag:
```bash
export UV_OFFLINE=1
export UV_CACHE_DIR=/opt/uv-cache
```
Hoặc truyền trực tiếp:
```bash
uv sync --offline --cache-dir=/opt/uv-cache
```
> Nếu có dependencies Git trong `tool.uv.sources`, bạn có thể thêm `--no-sources` để ngăn uv cố fetch mạng.

#### Chạy cài đặt Offline
```bash
uv sync --offline
```
uv sẽ sử dụng cache, lockfile, và cài đặt toàn bộ Python + main/dev dependencies mà không cần truy cập Internet.

### 3. Đảm bảo 100% đồng nhất
- **Python Interpreter**: Pin trong `pyproject.toml` hoặc dùng `uv python install`.
- **uv Version**: Ghi lại và sử dụng `uv self update` trên cả hai máy.
- **pip Version**: Sau khi sync, kiểm tra `pip --version` trong venv.
- **Lockfile**: Luôn commit `uv.lock` sau khi thêm/bớt package.
- **Cache Integrity**: Chuyển toàn bộ thư mục cache nguyên vẹn (ví dụ bằng tarball).
- **Env Vars / Flags**: `UV_OFFLINE=1` và `UV_CACHE_DIR` (hoặc `--offline`/`--cache-dir`) để ép chế độ offline.

Với quy trình này, bạn tận dụng tối đa **cache** và **lockfile** của uv, đảm bảo môi trường offline tái lập đúng 100% so với máy có mạng.
### 4. Một số tuỳ chọn command uv

```bash
# uv sẽ resolve tất cả các dependency-groups đang định nghĩa (bao gồm `dev`) cùng lúc và ghi vào `uv.lock`
uv lock

# Cài cả dependencies chính lẫn `dev` theo lockfile
uv sync

# Nếu bạn muốn chỉ cài `dev` mà không cài project chính, dùng:
uv sync --only-dev   # tương đương --only-group dev

# còn để loại trừ dev, dùng `--no-dev` hoặc `--no-group dev`
uv sync --no-dev

# sync không bao gồm default-group
uv sync --no-default-groups

# hoặc chỉ định rõ ràng 1 group (bao gồm default-groups)
uv sync --group prod

# hoặc chỉ định rõ ràng 1 group (không bao gồm default-groups)
uv sync --no-default-groups --group prod

# chỉ cài đặt 1 group
uv sync --only-group prod

# Để cài tất cả nhóm đã định nghĩa, có thể nằm ngoài default-groups (ví dụ `dev`, `test`, `docs`, …), sử dụng:
uv sync --all-groups
```

### 5. Đóng gói môi trường offline - docker image

Tận dụng **Docker BuildKit cache mounts** kết hợp với cơ chế **aggressive caching** của `uv` để xây dựng image hoàn toàn offline, vẫn cài được đầy đủ dependencies từ cache mà không phải tải lại từ mạng, đồng thời tách riêng các layer phụ thuộc và dọn sạch cache trước khi đóng gói final image để giảm tối đa dung lượng [docs.docker.com](https://docs.docker.com/build/cache/optimize/)[docs.astral.sh](https://docs.astral.sh/uv/guides/integration/docker/)

**Quy trình gồm:**
**(1)** trên máy online chạy `uv sync` để đổ đầy cache;
**(2)** sao chép cache đó sang máy build offline;
**(3)** trong Dockerfile dùng `RUN --mount=type=cache,target=/root/.cache/uv` và flag `--offline`/`UV_OFFLINE` cho uv;
**(4)** dùng multi-stage build để không mang cả cache vào final image

#### Chuẩn bị cache trên máy Online

1. **Tạo lockfile với URL cố định**
	```
	uv lock
	```
	để tạo `uv.lock` bao gồm **URL wheel/sdist** đã được pin cứng, tránh query index khi offline
2. **Sync và load full cache**
	1. Thiết lập thư mục cache (nếu muốn): `export UV_CACHE_DIR=./offline-cache`
	2. Chạy sync để uv sẽ tải về và lưu mọi wheel, sdist, Git archive… vào `./offline-cache` (hoặc `$XDG_CACHE_HOME/uv`): `uv sync`
3. **Đóng gói cache**:
	Sau khi sync xong, **tar/zip** toàn bộ folder `offline-cache/` để chuyển sang môi trường build offline.
#### Sử dụng cache trong Docker Build
1. **Kích hoạt Docker BuildKit**
	Đảm bảo bật BuildKit, do BuildKit hỗ trợ `--mount=type=cache` để lưu cache ngoài image: `export DOCKER_BUILDKIT=1`
2. **Dockerfile với cache mount**
```Dockerfile
# syntax=docker/dockerfile:1.4
FROM ghcr.io/astral-sh/uv:debian-slim AS builder

WORKDIR /app
COPY pyproject.toml uv.lock ./

# Mount cache uv, sync dependencies (main + dev)
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked

# Tách stage final, chỉ copy venv/venv-tools đã sẵn sàng
FROM python:3.11-slim
COPY --from=builder /root/.cache/uv /root/.cache/uv
COPY --from=builder /app/.venv /app/.venv

ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
COPY . .

# Chạy lệnh offline (sẽ dùng cache đã mount)
RUN uv sync --offline --locked

CMD ["uv", "run", "my_app"]

```
- `--mount=type=cache,target=/root/.cache/uv` giữ cache ngoài image, không làm tăng size layer [docs.astral.sh](https://docs.astral.sh/uv/guides/integration/docker/?utm_source=chatgpt.com).
- Stage `builder` tải và cài vào virtualenv, stage cuối chỉ copy ra thành phẩm, không mang theo cache dư thừa [rhosignal.com](https://www.rhosignal.com/posts/uv-in-docker/?utm_source=chatgpt.com).

#### Cài đặt Offline trong Build
1. **Cấu hình biến môi trường**
	Trong Dockerfile hoặc CI, trước khi chạy uv, thiết lập để ép uv chỉ dùng cache, không attempt kết nối Internet
	```dockerfile
	ENV UV_OFFLINE=1
	ENV UV_CACHE_DIR=/root/.cache/uv
	```
2. **Chạy uv sync offline**
```dockerfile
RUN uv sync --offline --locked
```
uv sẽ đọc `uv.lock`, lookup đúng wheel/sdist trong cache và cài toàn bộ main + dev dependencies mà không cần mạng

#### Tối ưu dung lượng image
##### 4.1. Multi-stage builds

Tách riêng stage **builder** và **runtime** để chỉ copy virtualenv (hoặc wheelhouse) cần thiết vào final image, loại bỏ dữ liệu build/cache [depot.dev](https://depot.dev/docs/container-builds/how-to-guides/optimal-dockerfiles/python-uv-dockerfile?utm_source=chatgpt.com).
##### 4.2. Xoá cache trước khi đóng gói

Nếu bạn không dùng multi-stage, có thể chạy:
```dockerfile
`RUN uv cache clean`
```
sau khi sync thành công để xoá cache trong container trước khi commit image [medium.com](https://medium.com/%40benitomartin/deep-dive-into-uv-dockerfiles-by-astral-image-size-performance-best-practices-5790974b9579?utm_source=chatgpt.com).
##### 4.3. Sử dụng `.dockerignore`

Loại bỏ toàn bộ thư mục cache và `.venv` (nếu có) trong context:
```.dockerignore
offline-cache/
.venv/
```
giúp **giảm size context** và tránh vô tình ADD cache vào image [rhosignal.com](https://www.rhosignal.com/posts/uv-in-docker/?utm_source=chatgpt.com).

#### Hướng tiếp cận: đóng gói image online và chuyển giao image offline

Ngoài hướng tiếp cận đóng gói trên môi trường offline thì còn hướng tiếp cận khác là hoàn toàn có thể **xây dựng (build) Docker image** trên máy có mạng rồi **chuyển** sang máy offline mà vẫn giữ nguyên mọi layer và metadata. Cách phổ biến nhất là dùng hai lệnh `docker save` và `docker load` để đóng gói image thành file `.tar`, sau đó copy qua USB/ổ cứng ngoài và load lên máy offline [serverfault.com](https://serverfault.com/questions/701248/downloading-docker-image-for-transfer-to-non-internet-connected-machine?utm_source=chatgpt.com).

Ngoài ra, bạn còn có thể thiết lập một **registry nội bộ** (Harbor, Nexus, Artifactory…) để push/pull image trong mạng LAN mà không cần Internet

##### Chuyển image bằng `docker save` / `docker load`

1. **Build hoặc pull image trên máy online**
Build
```bash
docker build -t myapp:latest .
```
hoặc pull
```bash
docker pull ubuntu:22.04
```

2. **Đóng gói image thành file TAR**
```bash
docker save -o myapp_latest.tar myapp:latest
```
Lệnh này sẽ gom toàn bộ layers, tags, metadata của `myapp:latest` vào file `myapp_latest.tar` [serverfault.com](https://serverfault.com/questions/701248/downloading-docker-image-for-transfer-to-non-internet-connected-machine?utm_source=chatgpt.com).

3. **Chuyển file TAR sang máy offline**
Sử dụng USB, ổ cứng di động hoặc giao thức nội bộ (SCP qua mạng LAN) để copy file `myapp_latest.tar` sang máy không có Internet

4. **Load image trên máy offline**
```bash
docker load -i myapp_latest.tar
```
Sau khi chạy, image `myapp:latest` sẽ xuất hiện trong local registry của Docker offline

5. Chạy container
```bash
docker run --rm myapp:latest
```
Container sẽ khởi động như thông thường, bất chấp máy không có kết nối ra bên ngoài

##### Dùng registry nội bộ (Local Registry)

1. **Khởi động registry cục bộ**

    `docker run -d -p 5000:5000 --restart=always --name registry registry:2`

    Tạo một Docker Registry đơn giản chạy tại `localhost:5000` [forums.docker.com](https://forums.docker.com/t/caching-and-building-offline/143656?utm_source=chatgpt.com).

2. **Tag và push image vào registry**

    `docker tag myapp:latest localhost:5000/myapp:latest docker push localhost:5000/myapp:latest`

    Image sẽ được lưu trữ trên registry nội bộ, không cần ra Docker Hub [gcore.com](https://gcore.com/learning/how-to-transfer-move-a-docker-image-to-another-system?utm_source=chatgpt.com).

3. **Truy cập registry trên máy offline**

    - Copy toàn bộ dữ liệu registry (thư mục `/var/lib/registry`) sang máy offline,
    - Chạy registry offline với cùng version và dữ liệu đã copy.
        Registry nội bộ sẽ phục vụ lệnh `docker pull localhost:5000/myapp:latest` mà không cần Internet [blog.ctms.me](https://blog.ctms.me/posts/2025-01-12-forgejo-docker-registry-offline-offgrid/?utm_source=chatgpt.com).

4. **Pull image từ registry offline**

    `docker pull localhost:5000/myapp:latest`

    Image được tải từ registry LAN, hoàn toàn offline [mpolinowski.github.io](https://mpolinowski.github.io/docs/DevOps/Linux/2019-06-14--download-and-save-docker-image/2019-06-14/?utm_source=chatgpt.com).
