Quản lý Môi trường Python

Contents

1.1.3. 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.

1.1.3.1. 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:

# [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.)

1.1.3.2. 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.tomllock 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.lockchứa đúng phiên bản và cây phụ thuộc đầy đủ của chúng. Nói cách khác, uv.locklà 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:

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

1.1.3.2.1.1. 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:

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.

  1. 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à:

        [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 lockuv 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.

1.1.3.2.1.2. 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.

1.1.3.2.2. 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.

1.1.3.3. 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 syncsẽ 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.

1.1.3.4. 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:

1.1.3.4.1. 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ó) 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.

1.1.3.4.2. 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)  uv.lock (nơi thể hiện kết quả phiên bản cụ thể). Ví dụ:

    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.

1.1.3.4.3. 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.

1.1.3.4.4. 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 lockuv adduv 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.

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.

1.1.3.5. 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.

Sau đó, mang cả projectthư mục cache sang máy offline, thiết lập UV_OFFLINE=1UV_CACHE_DIR trỏ tới cache đó build.opensuse.org, 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.1.3.5.1. 1. Trên máy Online#

1.1.3.5.1.1. Tạo pyproject.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.)

1.1.3.5.1.2. Pin Python, uv và pip#

1.1.3.5.1.2.1. 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

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

1.1.3.5.1.3. 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.

# 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

1.1.3.5.1.4. Sync và tạo cache#

  1. Xác định hoặc tuỳ chỉnh nơi lưu cache:

    UV_CACHE_DIR=./offline-cache
    
  2. Chạy:

    uv sync
    

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.

  1. (Tuỳ chọn) Kiểm tra/thu dọn cache:

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

1.1.3.5.1.5. Đó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.

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

1.1.3.5.2.1. Clone source code#

git clone https://gitlab.internal/your-project.git
cd your-project

Đảm bảo dự án có pyproject.tomluv.lock đã commit.

1.1.3.5.2.2. 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.

1.1.3.5.2.3. Cấu hình uv offline#

Thiết lập biến môi trường hoặc flag:

export UV_OFFLINE=1
export UV_CACHE_DIR=/opt/uv-cache

Hoặc truyền trực tiếp:

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.

1.1.3.5.2.4. Chạy cài đặt Offline#

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.

1.1.3.5.3. 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=1UV_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 cachelockfile 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.

1.1.3.5.4. 4. Một số tuỳ chọn command uv#

# 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

1.1.3.5.5. 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.comdocs.astral.sh

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

1.1.3.5.5.1. 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.

1.1.3.5.5.2. 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

# 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.

  • 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.

1.1.3.5.5.3. 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

    ENV UV_OFFLINE=1
    ENV UV_CACHE_DIR=/root/.cache/uv
    
  2. Chạy uv sync offline

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

1.1.3.5.5.4. Tối ưu dung lượng image#

1.1.3.5.5.4.1. 4.1. Multi-stage builds#

Tách riêng stage builderruntime để chỉ copy virtualenv (hoặc wheelhouse) cần thiết vào final image, loại bỏ dữ liệu build/cache depot.dev.

1.1.3.5.5.4.2. 4.2. Xoá cache trước khi đóng gói#

Nếu bạn không dùng multi-stage, có thể chạy:

`RUN uv cache clean`

sau khi sync thành công để xoá cache trong container trước khi commit image medium.com.

1.1.3.5.5.4.3. 4.3. Sử dụng .dockerignore#

Loại bỏ toàn bộ thư mục cache và .venv (nếu có) trong context:

offline-cache/
.venv/

giúp giảm size context và tránh vô tình ADD cache vào image rhosignal.com.

1.1.3.5.5.5. 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 savedocker 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.

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

1.1.3.5.5.5.1. Chuyển image bằng docker save / docker load#
  1. Build hoặc pull image trên máy online Build

docker build -t myapp:latest .

hoặc pull

docker pull ubuntu:22.04
  1. Đóng gói image thành file TAR

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.

  1. 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

  2. Load image trên máy offline

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

  1. Chạy container

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

1.1.3.5.5.5.2. 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.

  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.

  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.

  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.