paint-brush
Cách tạo chương trình Python CLI để quản lý bảng Trello (Phần 2)từ tác giả@elainechan01
1,672 lượt đọc
1,672 lượt đọc

Cách tạo chương trình Python CLI để quản lý bảng Trello (Phần 2)

từ tác giả Elaine Yun Ru Chan27m2023/11/07
Read on Terminal Reader

dài quá đọc không nổi

Phần 2 của loạt bài hướng dẫn về Cách tạo Chương trình CLI Python cho Quản lý bảng Trello tập trung vào cách viết logic nghiệp vụ cho các lệnh CLI và phân phối gói Python
featured image - Cách tạo chương trình Python CLI để quản lý bảng Trello (Phần 2)
Elaine Yun Ru Chan HackerNoon profile picture
0-item

Bây giờ chúng ta đã vượt xa dự án trường học oẳn tù tì chủ yếu - hãy đi sâu vào dự án đó.


Chúng ta sẽ đạt được gì thông qua hướng dẫn này?

Trong Cách tạo chương trình Python CLI để quản lý bảng Trello (Phần 1) , chúng tôi đã tạo thành công logic nghiệp vụ để tương tác với Trello SDK.


Dưới đây là bản tóm tắt nhanh về kiến trúc của chương trình CLI của chúng tôi:

Xem bảng chi tiết về cấu trúc CLI dựa trên yêu cầu


Trong hướng dẫn này, chúng ta sẽ xem cách chuyển đổi dự án của mình thành chương trình CLI, tập trung vào các yêu cầu chức năng và phi chức năng.


Mặt khác, chúng ta cũng sẽ học cách phân phối chương trình của mình dưới dạng gói trên PyPI .


Bắt đầu nào


Cấu trúc thư mục

Trước đây, chúng tôi đã thiết lập được bộ khung để lưu trữ mô-đun trelloservice của mình. Lần này, chúng tôi muốn triển khai một thư mục cli với các mô-đun cho các chức năng khác nhau, cụ thể là:


  • cấu hình
  • truy cập
  • danh sách


Ý tưởng là, đối với mỗi nhóm lệnh, các lệnh của nó sẽ được lưu trữ trong mô-đun riêng. Đối với lệnh list , chúng tôi sẽ lưu trữ nó trong tệp CLI chính vì nó không thuộc bất kỳ nhóm lệnh nào.


Mặt khác, chúng ta hãy xem xét việc dọn dẹp cấu trúc thư mục của chúng ta. Cụ thể hơn, chúng ta nên bắt đầu tính toán khả năng mở rộng của phần mềm bằng cách đảm bảo rằng các thư mục không bị lộn xộn.


Đây là một gợi ý về cấu trúc thư mục của chúng tôi:

 trellocli/ __init__.py __main__.py trelloservice.py shared/ models.py custom_exceptions.py cli/ cli.py cli_config.py cli_create.py tests/ test_cli.py test_trelloservice.py assets/ images/ README.md pyproject.toml .env .gitignore


Chú ý có thư mục assets như thế nào? Điều này sẽ được sử dụng để lưu trữ các nội dung liên quan cho README của chúng tôi trong khi có một thư mục shared mới được triển khai trong trellocli , chúng tôi sẽ sử dụng nó để lưu trữ các mô-đun sẽ được sử dụng trên phần mềm.


Cài đặt

Hãy bắt đầu bằng cách sửa đổi tệp điểm nhập của chúng tôi, __main__.py . Nhìn vào bản thân quá trình nhập, vì chúng tôi đã quyết định lưu trữ các mô-đun liên quan trong các thư mục con của riêng chúng nên chúng tôi sẽ phải điều chỉnh những thay đổi đó. Mặt khác, chúng tôi cũng giả định rằng mô-đun CLI chính, cli.py , có một phiên bản app mà chúng tôi có thể chạy.

 # trellocli/__main__.py # module imports from trellocli import __app_name__ from trellocli.cli import cli from trellocli.trelloservice import TrelloService # dependencies imports # misc imports def main(): cli.app(prog_name=__app_name__) if __name__ == "__main__": main()


Chuyển nhanh tới tệp cli.py của chúng tôi; chúng tôi sẽ lưu trữ phiên bản app của chúng tôi ở đây. Ý tưởng là khởi tạo một đối tượng Typer để chia sẻ trên phần mềm.

 # trellocli/cli/cli.py # module imports # dependencies imports from typer import Typer # misc imports # singleton instances app = Typer()


Tiếp tục với khái niệm này, hãy sửa đổi pyproject.toml để chỉ định các tập lệnh dòng lệnh của chúng tôi. Tại đây, chúng tôi sẽ cung cấp tên cho gói của mình và xác định điểm vào.

 # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main"


Dựa trên mẫu ở trên, chúng tôi đã xác định trellocli là tên gói và hàm main trong tập lệnh __main__ , được lưu trữ trong mô-đun trellocli sẽ được thực thi trong thời gian chạy.


Bây giờ phần CLI của phần mềm đã được thiết lập, hãy sửa đổi mô-đun trelloservice để phục vụ chương trình CLI tốt hơn. Như bạn nhớ lại, mô-đun trelloservice của chúng tôi được thiết lập để yêu cầu ủy quyền của người dùng một cách đệ quy cho đến khi được phê duyệt. Chúng tôi sẽ sửa đổi điều này để chương trình sẽ thoát nếu không được cấp quyền và khuyến khích người dùng chạy lệnh config access . Điều này sẽ đảm bảo rằng chương trình của chúng tôi rõ ràng hơn và mang tính mô tả hơn về mặt hướng dẫn.


Để diễn đạt điều này thành lời, chúng tôi sẽ sửa đổi các chức năng này:


  • __init__
  • __load_oauth_token_env_var
  • authorize
  • is_authorized


Bắt đầu với hàm __init__ , chúng ta sẽ khởi tạo một ứng dụng khách trống thay vì xử lý việc thiết lập ứng dụng khách tại đây.

 # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None


Góc thử thách 💡Bạn có thể sửa đổi hàm __load_oauth_token_env_var của chúng tôi để nó không nhắc đệ quy về ủy quyền của người dùng không? Gợi ý: Hàm đệ quy là hàm gọi chính nó.


Chuyển sang các hàm trợ giúp authorizeis_authorized , ý tưởng là authorize sẽ thực hiện logic nghiệp vụ của việc thiết lập ứng dụng khách bằng cách sử dụng hàm __load_oauth_token_env_var trong khi hàm is_authorized chỉ trả về giá trị boolean cho biết liệu ủy quyền có được cấp hay không.

 # trellocli/trelloservice.py class TrelloService: def authorize(self) -> AuthorizeResponse: """Method to authorize program to user's trello account Returns AuthorizeResponse: success / error """ self.__load_oauth_token_env_var() load_dotenv() if not os.getenv("TRELLO_OAUTH_TOKEN"): return AuthorizeResponse(status_code=TRELLO_AUTHORIZATION_ERROR) else: self.__client = TrelloClient( api_key=os.getenv("TRELLO_API_KEY"), api_secret=os.getenv("TRELLO_API_SECRET"), token=os.getenv("TRELLO_OAUTH_TOKEN") ) return AuthorizeResponse(status_code=SUCCESS) def is_authorized(self) -> bool: """Method to check authorization to user's trello account Returns bool: authorization to user's account """ if not self.__client: return False else: return True


Hiểu rằng sự khác biệt giữa __load_oauth_token_env_varauthorize__load_oauth_token_env_var là một hàm đối diện nội bộ dùng để lưu trữ mã thông báo ủy quyền dưới dạng biến môi trường trong khi authorize là chức năng đối mặt công khai, nó cố gắng truy xuất tất cả thông tin xác thực cần thiết và khởi tạo Ứng dụng khách Trello.


Góc thử thách 💡Lưu ý cách hàm authorize của chúng tôi trả về loại dữ liệu AuthorizeResponse . Bạn có thể triển khai mô hình có thuộc tính status_code không? Tham khảo Phần 1 của Cách tạo chương trình Python CLI để quản lý bảng Trello (Gợi ý: Hãy xem cách chúng tôi tạo Mô hình)


Cuối cùng, hãy khởi tạo một đối tượng TrelloService đơn lẻ ở cuối mô-đun. Vui lòng tham khảo bản vá này để xem mã đầy đủ trông như thế nào: trello-cli-kit

 # trellocli/trelloservice.py trellojob = TrelloService()


Cuối cùng, chúng tôi muốn khởi tạo một số ngoại lệ tùy chỉnh để chia sẻ trong chương trình. Điều này khác với ERRORS được xác định trong trình khởi tạo của chúng tôi vì các ngoại lệ này là các lớp con từ BaseException và hoạt động như các ngoại lệ điển hình do người dùng xác định, trong khi ERRORS phục vụ nhiều hơn dưới dạng các giá trị không đổi bắt đầu từ 0.


Hãy giữ các ngoại lệ của chúng ta ở mức tối thiểu và thực hiện một số trường hợp sử dụng phổ biến, đáng chú ý nhất là:


  • Lỗi đọc: Xảy ra khi có lỗi đọc từ Trello
  • Lỗi ghi: Xảy ra khi có lỗi ghi vào Trello
  • Lỗi ủy quyền: Xảy ra khi ủy quyền không được cấp cho Trello
  • Lỗi đầu vào của người dùng không hợp lệ: Xảy ra khi đầu vào CLI của người dùng không được nhận dạng


 # trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass


Kiểm tra đơn vị

Như đã đề cập trong Phần I, chúng ta sẽ không trình bày rộng rãi về Unit Test trong hướng dẫn này, vì vậy hãy chỉ làm việc với các phần tử cần thiết:


  • Kiểm tra để cấu hình quyền truy cập
  • Test cấu hình bảng trello
  • Test tạo thẻ trello mới
  • Kiểm tra hiển thị chi tiết bảng trello
  • Test hiển thị chi tiết bảng trello (xem chi tiết)


Ý tưởng là mô phỏng một trình thông dịch dòng lệnh, giống như một shell để kiểm tra kết quả mong đợi. Điều tuyệt vời ở mô-đun Typer là nó đi kèm với đối tượng runner riêng. Để chạy thử nghiệm, chúng tôi sẽ ghép nối nó với mô-đun pytest . Để biết thêm thông tin, hãy xem tài liệu chính thức của Typer .


Chúng ta hãy cùng nhau thực hiện bài kiểm tra đầu tiên, tức là định cấu hình quyền truy cập. Hiểu rằng chúng tôi đang kiểm tra xem hàm có thực thi đúng cách hay không. Để làm như vậy, chúng tôi sẽ kiểm tra phản hồi của hệ thống và liệu mã thoát có success hay còn gọi là 0. Đây là một bài viết hay của RedHat về mã thoát là gì và cách hệ thống sử dụng chúng để giao tiếp với các quy trình .

 # trellocli/tests/test_cli.py # module imports from trellocli.cli.cli import app # dependencies imports from typer.testing import CliRunner # misc imports runner = CliRunner() def test_config_access(): res = runner.invoke(app, ["config", "access"]) assert result.exit_code == 0 assert "Go to the following link in your browser:" in result.stdout


Góc thử thách 💡Bây giờ bạn đã nắm được ý chính, bạn có thể tự mình thực hiện các trường hợp thử nghiệm khác không? (Gợi ý: bạn cũng nên xem xét việc kiểm tra các trường hợp lỗi)


Logic kinh doanh


Mô-đun CLI chính

Hãy hiểu rằng đây sẽ là mô-đun cli chính của chúng tôi - đối với tất cả các nhóm lệnh (cấu hình, tạo), logic nghiệp vụ của chúng sẽ được lưu trữ trong tệp riêng để dễ đọc hơn.


Trong mô-đun này, chúng tôi sẽ lưu trữ lệnh list của mình. Đi sâu hơn vào lệnh, chúng tôi biết rằng chúng tôi muốn triển khai các tùy chọn sau:


  • board_name: bắt buộc nếu config board chưa được đặt trước đó
  • chi tiết: hiển thị ở chế độ xem chi tiết


Bắt đầu với tùy chọn bắt buộc board_name, có một số cách để đạt được điều này, một trong số đó là sử dụng hàm gọi lại (Để biết thêm thông tin, đây là tài liệu chính thức ) hoặc đơn giản là sử dụng biến môi trường mặc định. Tuy nhiên, đối với trường hợp sử dụng của chúng ta, hãy đơn giản hóa vấn đề bằng cách đưa ra ngoại lệ tùy chỉnh InvalidUserInputError nếu các điều kiện không được đáp ứng.


Để xây dựng lệnh, hãy bắt đầu bằng việc xác định các tùy chọn. Trong Typer, như đã đề cập trong tài liệu chính thức của họ, các thành phần chính để xác định một tùy chọn sẽ là:


  • Loại dữ liệu
  • Văn bản trợ giúp
  • Giá trị mặc định


Ví dụ: để tạo tùy chọn detailed với các điều kiện sau:


  • Kiểu dữ liệu: bool
  • Văn bản trợ giúp: “Bật chế độ xem chi tiết”
  • Giá trị mặc định: Không có


Mã của chúng tôi sẽ trông như thế này:

 detailed: Annotated[bool, typer.Option(help=”Enable detailed view)] = None


Nhìn chung, để xác định lệnh list với các tùy chọn cần thiết, chúng ta sẽ coi list là một Hàm Python và các tùy chọn của nó là các tham số bắt buộc.

 # trellocli/cli/cli.py @app.command() def list( detailed: Annotated[bool, Option(help="Enable detailed view")] = None, board_name: Annotated[str, Option(help="Trello board to search")] = "" ) -> None: pass


Lưu ý rằng chúng tôi đang thêm lệnh vào phiên bản app được khởi tạo ở đầu tệp. Vui lòng điều hướng qua cơ sở mã Typer chính thức để sửa đổi Tùy chọn theo ý thích của bạn.


Đối với quy trình làm việc của lệnh, chúng tôi sẽ thực hiện một số việc như thế này:


  • Kiểm tra ủy quyền
  • Định cấu hình để sử dụng bảng thích hợp (kiểm tra xem tùy chọn board_name đã được cung cấp chưa)
  • Thiết lập bảng Trello để đọc từ
  • Truy xuất dữ liệu thẻ Trello thích hợp và phân loại dựa trên danh sách Trello
  • Hiển thị dữ liệu (kiểm tra xem tùy chọn detailed đã được chọn chưa)


Một số điều cần lưu ý…


  • Chúng tôi muốn đưa ra Ngoại lệ khi trellojob tạo ra mã trạng thái khác với SUCCESS . Bằng cách sử dụng các khối try-catch , chúng tôi có thể ngăn chương trình của mình gặp sự cố nghiêm trọng.
  • Khi định cấu hình bảng thích hợp để sử dụng, chúng tôi sẽ cố gắng thiết lập bảng Trello để sử dụng dựa trên board_id đã truy xuất. Vì vậy, chúng tôi muốn đề cập đến các trường hợp sử dụng sau
    • Truy xuất board_id nếu board_name được cung cấp rõ ràng bằng cách kiểm tra sự trùng khớp bằng hàm get_all_boards trong trellojob
    • Truy xuất board_id được lưu dưới dạng biến môi trường nếu tùy chọn board_name không được sử dụng
  • Dữ liệu chúng tôi sẽ hiển thị sẽ được định dạng bằng chức năng Table từ gói rich . Để biết thêm thông tin về rich , vui lòng tham khảo tài liệu chính thức của họ
    • Chi tiết: Hiển thị tóm tắt về số lượng danh sách Trello, số lượng thẻ và nhãn được xác định. Đối với mỗi danh sách Trello, hiển thị tất cả các thẻ và tên, mô tả và nhãn liên quan tương ứng của chúng
    • Không chi tiết: Hiển thị tóm tắt số lượng danh sách Trello, số lượng thẻ và nhãn được xác định


Đặt mọi thứ lại với nhau, chúng ta có được một cái gì đó như sau. Tuyên bố từ chối trách nhiệm: có thể có một số chức năng bị thiếu trong TrelloService mà chúng tôi chưa triển khai. Vui lòng tham khảo bản vá này nếu bạn cần trợ giúp triển khai chúng: trello-cli-kit

 # trellocli/cli/cli.py # module imports from trellocli.trelloservice import trellojob from trellocli.cli import cli_config, cli_create from trellocli.misc.custom_exceptions import * from trellocli import SUCCESS # dependencies imports from typer import Typer, Option from rich import print from rich.console import Console from rich.table import Table from dotenv import load_dotenv # misc imports from typing_extensions import Annotated import os # singleton instances app = Typer() console = Console() # init command groups app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations") app.add_typer(cli_create.app, name="create", help="COMMAND GROUP to create new Trello elements") @app.command() def list( detailed: Annotated[ bool, Option(help="Enable detailed view") ] = None, board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to list board details in a simplified (default)/detailed view OPTIONS detailed (bool): request for detailed view board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError labels_list = res_get_all_labels.res res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError trellolists_list = res_get_all_lists.res # store data on cards for each trellolist trellolists_dict = {trellolist: [] for trellolist in trellolists_list} for trellolist in trellolists_list: res_get_all_cards = trellojob.get_all_cards(trellolist=trellolist) if res_get_all_cards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving cards from trellolist") raise TrelloReadError cards_list = res_get_all_cards.res trellolists_dict[trellolist] = cards_list # display data (lists count, cards count, labels) # if is_detailed OPTION is selected, display data (name, description, labels) for each card in each trellolist print() table = Table(title="Board: "+board.name, title_justify="left", show_header=False) table.add_row("[bold]Lists count[/]", str(len(trellolists_list))) table.add_row("[bold]Cards count[/]", str(sum([len(cards_list) for cards_list in trellolists_dict.values()]))) table.add_row("[bold]Labels[/]", ", ".join([label.name for label in labels_list if label.name])) console.print(table) if detailed: for trellolist, cards_list in trellolists_dict.items(): table = Table("Name", "Desc", "Labels", title="List: "+trellolist.name, title_justify="left") for card in cards_list: table.add_row(card.name, card.description, ", ".join([label.name for label in card.labels if label.name])) console.print(table) print() except (AuthorizationError, InvalidUserInputError, TrelloReadError): print("Program exited...")


Để xem phần mềm của chúng tôi hoạt động, chỉ cần chạy python -m trellocli --help trong thiết bị đầu cuối. Theo mặc định, mô-đun Typer sẽ tự điền đầu ra cho lệnh --help . Và hãy chú ý cách chúng ta có thể gọi trellocli làm tên gói - hãy nhớ lại cách định nghĩa trước đây trong pyproject.toml của chúng ta?


Hãy tua nhanh một chút và khởi tạo các nhóm lệnh createconfig . Để làm như vậy, chúng ta chỉ cần sử dụng hàm add_typer trên đối tượng app của mình. Ý tưởng là nhóm lệnh sẽ có đối tượng app riêng và chúng ta sẽ chỉ thêm đối tượng đó vào app chính trong cli.py , cùng với tên của nhóm lệnh và văn bản trợ giúp. Nó sẽ trông giống như thế này

 # trellocli/cli/cli.py app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations")


Góc thử thách 💡Bạn có thể tự nhập nhóm lệnh create không? Vui lòng tham khảo bản vá này để được trợ giúp: trello-cli-kit


Lệnh phụ

Để thiết lập một nhóm lệnh cho create , chúng tôi sẽ lưu trữ các lệnh tương ứng trong mô-đun riêng của nó. Thiết lập tương tự như cli.py với nhu cầu khởi tạo đối tượng Typer. Đối với các lệnh, chúng tôi cũng muốn tuân thủ nhu cầu sử dụng các ngoại lệ tùy chỉnh. Một chủ đề bổ sung mà chúng tôi muốn đề cập đến là khi người dùng nhấn Ctrl + C , hay nói cách khác, làm gián đoạn quá trình. Lý do tại sao chúng tôi không đề cập đến điều này trong lệnh list của mình là vì sự khác biệt ở đây là nhóm lệnh config bao gồm các lệnh tương tác. Sự khác biệt chính giữa các lệnh tương tác là chúng yêu cầu sự tương tác liên tục của người dùng. Tất nhiên, nói rằng lệnh trực tiếp của chúng tôi mất nhiều thời gian để thực thi. Đây cũng là cách tốt nhất để xử lý các ngắt quãng có thể xảy ra với bàn phím.


Bắt đầu với lệnh access , cuối cùng chúng ta sẽ sử dụng hàm authorize như được tạo trong TrelloService . Vì hàm authorize tự xử lý cấu hình nên chúng tôi sẽ chỉ phải xác minh việc thực thi quy trình.

 # trellocli/cli/cli_config.py @app.command() def access() -> None: """COMMAND to configure authorization for program to access user's Trello account""" try: # check authorization res_authorize = trellojob.authorize() if res_authorize.status_code != SUCCESS: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except AuthorizationError: print("Program exited...")


Đối với lệnh board , chúng tôi sẽ sử dụng nhiều mô-đun khác nhau để mang lại trải nghiệm tốt cho người dùng, bao gồm Menu thiết bị đầu cuối đơn giản để hiển thị GUI đầu cuối cho tương tác của người dùng. Ý tưởng chính là như sau:


  • Kiểm tra ủy quyền
  • Truy xuất tất cả bảng Trello từ tài khoản của người dùng
  • Hiển thị menu đầu cuối lựa chọn duy nhất của bảng Trello
  • Đặt ID bảng Trello đã chọn làm biến môi trường


 # trellocli/cli/cli_config.py @app.command() def board() -> None: """COMMAND to initialize Trello board""" try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # retrieve all boards res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving trello boards") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # for easy access to board id when given board name # display menu to select board boards_menu = TerminalMenu( boards_list.keys(), title="Select board:", raise_error_on_interrupt=True ) boards_menu.show() selected_board = boards_menu.chosen_menu_entry # set board ID as env var dotenv_path = find_dotenv() set_key( dotenv_path=dotenv_path, key_to_set="TRELLO_BOARD_ID", value_to_set=boards_list[selected_board] ) except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, TrelloReadError): print("Program exited...")


Cuối cùng, chúng ta sẽ chuyển sang yêu cầu chức năng cốt lõi của phần mềm - Thêm thẻ mới vào danh sách trên bảng Trello. Chúng tôi sẽ sử dụng các bước tương tự từ lệnh list cho đến khi lấy dữ liệu từ bảng.


Ngoài ra, chúng tôi sẽ yêu cầu người dùng cung cấp thông tin tương tác để định cấu hình thẻ mới đúng cách:


  • Danh sách Trello sẽ được thêm vào: Lựa chọn một lần
  • Tên thẻ: Văn bản
  • [Tùy chọn] Mô tả thẻ: Văn bản
  • [Tùy chọn] Nhãn: Nhiều lựa chọn
  • Xác nhận: có/không


Đối với tất cả các lời nhắc cần người dùng chọn từ danh sách, chúng tôi sẽ sử dụng gói Simple Terminal Menu như trước đây. Đối với các lời nhắc và mục linh tinh khác như cần nhập văn bản hoặc xác nhận của người dùng, chúng tôi sẽ chỉ sử dụng gói rich . Điều quan trọng cần lưu ý là chúng ta phải xử lý đúng các giá trị tùy chọn:


  • Người dùng có thể bỏ qua việc cung cấp mô tả
  • Người dùng có thể cung cấp lựa chọn trống cho Nhãn


 # trellocli/cli/cli_create.py @app.command() def card( board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to add a new trello card OPTIONS board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the labels from the trello board") raise TrelloReadError labels_list = res_get_all_labels.res labels_dict = {label.name: label for label in labels_list if label.name} res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the lists from the trello board") raise TrelloReadError trellolists_list = res_get_all_lists.res trellolists_dict = {trellolist.name: trellolist for trellolist in trellolists_list} # for easy access to trellolist when given name of trellolist # request for user input (trellolist, card name, description, labels to include) interactively to configure new card to be added trellolist_menu = TerminalMenu( trellolists_dict.keys(), title="Select list:", raise_error_on_interrupt=True ) # Prompt: trellolist trellolist_menu.show() print(trellolist_menu.chosen_menu_entry) selected_trellolist = trellolists_dict[trellolist_menu.chosen_menu_entry] selected_name = Prompt.ask("Card name") # Prompt: card name selected_desc = Prompt.ask("Description (Optional)", default=None) # Prompt (Optional) description labels_menu = TerminalMenu( labels_dict.keys(), title="Select labels (Optional):", multi_select=True, multi_select_empty_ok=True, multi_select_select_on_accept=False, show_multi_select_hint=True, raise_error_on_interrupt=True ) # Prompt (Optional): labels labels_menu.show() selected_labels = [labels_dict[label] for label in list(labels_menu.chosen_menu_entries)] if labels_menu.chosen_menu_entries else None # display user selection and request confirmation print() confirmation_table = Table(title="Card to be Added", show_header=False) confirmation_table.add_row("List", selected_trellolist.name) confirmation_table.add_row("Name", selected_name) confirmation_table.add_row("Description", selected_desc) confirmation_table.add_row("Labels", ", ".join([label.name for label in selected_labels]) if selected_labels else None) console.print(confirmation_table) confirm = Confirm.ask("Confirm") # if confirm, attempt to add card to trello # else, exit if confirm: res_add_card = trellojob.add_card( col=selected_trellolist, name=selected_name, desc=selected_desc, labels=selected_labels ) if res_add_card.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when adding a new card to trello") raise TrelloWriteError else: print("Process cancelled...") except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, InvalidUserInputError, TrelloReadError, TrelloWriteError): print("Program exited...")


Góc thử thách 💡Bạn có thể hiển thị thanh tiến trình cho quá trình add không? Gợi ý: Hãy xem cách sử dụng tính năng trạng thái của rich


Phân phối gói

Phần thú vị nhất đến đây - phân phối chính thức phần mềm của chúng tôi trên PyPI. Chúng tôi sẽ theo đường dẫn này để làm như vậy:


  • Định cấu hình siêu dữ liệu + cập nhật README
  • Tải lên để kiểm tra PyPI
  • Định cấu hình hành động GitHub
  • Đẩy mã vào Tag v1.0.0
  • Phân phối mã cho PyPI 🎉


Để được giải thích chi tiết, hãy xem hướng dẫn tuyệt vời này về Python Packaging của Ramit Mittal.


Cấu hình siêu dữ liệu

Chi tiết cuối cùng mà chúng ta cần cho pyproject.toml là chỉ định mô-đun nào lưu trữ gói đó. Trong trường hợp của chúng tôi, đó sẽ là trellocli . Đây là siêu dữ liệu cần thêm:

 # pyproject.toml [tool.setuptools] packages = ["trellocli"]


Đối với README.md của chúng tôi, cách tốt nhất là cung cấp một số loại hướng dẫn, có thể là hướng dẫn sử dụng hoặc cách bắt đầu. Nếu bạn đã đưa hình ảnh vào README.md thì bạn nên sử dụng URL tuyệt đối của nó, thường có định dạng sau

 https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image>


TestPyPI

Chúng tôi sẽ sử dụng các công cụ buildtwine để xây dựng và xuất bản gói của mình. Chạy lệnh sau trong thiết bị đầu cuối của bạn để tạo kho lưu trữ nguồn và bánh xe cho gói của bạn:

 python -m build


Đảm bảo rằng bạn đã thiết lập tài khoản trên TestPyPI và chạy lệnh sau

 twine upload -r testpypi dist/*


Bạn sẽ được nhắc nhập tên người dùng và mật khẩu của mình. Do đã bật xác thực hai yếu tố, bạn sẽ được yêu cầu sử dụng Mã thông báo API (Để biết thêm thông tin về cách nhận mã thông báo API TestPyPI: liên kết đến tài liệu ). Đơn giản chỉ cần đặt vào các giá trị sau:


  • tên tài khoản: mã thông báo
  • mật khẩu: <mã thông báo TestPyPI của bạn>


Sau khi hoàn tất, bạn có thể truy cập TestPyPI để kiểm tra gói mới được phân phối của mình!


Cài đặt GitHub

Mục tiêu là sử dụng GitHub như một phương tiện để cập nhật liên tục các phiên bản mới của gói dựa trên thẻ.


Trước tiên, hãy đi tới tab Actions trên quy trình làm việc GitHub của bạn và chọn quy trình công việc mới. Chúng tôi sẽ sử dụng quy trình công việc Publish Python Package được tạo bởi GitHub Actions. Lưu ý rằng quy trình làm việc yêu cầu đọc từ kho lưu trữ bí mật như thế nào? Đảm bảo rằng bạn đã lưu trữ mã thông báo PyPI của mình dưới tên được chỉ định (Việc nhận mã thông báo API PyPI tương tự như TestPyPI).


Sau khi tạo quy trình làm việc, chúng tôi sẽ đẩy mã của mình vào thẻ v1.0.0. Để biết thêm thông tin về cú pháp đặt tên phiên bản, đây là lời giải thích tuyệt vời của Py-Pkgs: link to document


Đơn giản chỉ cần chạy các pull , addcommit thông thường. Tiếp theo, tạo thẻ cho cam kết mới nhất của bạn bằng cách chạy lệnh sau (Để biết thêm thông tin về thẻ: liên kết đến tài liệu )

 git tag <tagname> HEAD


Cuối cùng, đẩy thẻ mới của bạn vào kho lưu trữ từ xa

 git push <remote name> <tag name>


Đây là một bài viết tuyệt vời của Karol Horosin về Tích hợp CI/CD với Gói Python của bạn nếu bạn muốn tìm hiểu thêm. Nhưng bây giờ, hãy ngồi lại và tận hưởng thành tích mới nhất của bạn 🎉. Vui lòng xem điều kỳ diệu được làm sáng tỏ dưới dạng quy trình làm việc của Hành động GitHub khi nó phân phối gói của bạn tới PyPI.


Gói (lại

Đây là một cái dài 😓. Thông qua hướng dẫn này, bạn đã học cách chuyển đổi phần mềm của mình thành chương trình CLI bằng cách sử dụng mô-đun Typer và phân phối gói của bạn tới PyPI. Để tìm hiểu sâu hơn, bạn đã học cách xác định lệnh và nhóm lệnh, phát triển phiên CLI tương tác và tìm hiểu các tình huống CLI phổ biến như gián đoạn bàn phím.


Bạn quả thực là một phù thủy tuyệt vời trong việc vượt qua tất cả. Bạn sẽ không tham gia cùng tôi trong Phần 3, nơi chúng tôi triển khai các chức năng tùy chọn chứ?