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 đó.
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:
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 .
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à:
Ý 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.
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 authorize
và is_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_var
và authorize
là __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à:
# trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass
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:
Ý 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)
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:
config board
chưa được đặt trước đó
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à:
Ví dụ: để tạo tùy chọn detailed
với các điều kiện sau:
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:
board_name
đã được cung cấp chưa)detailed
đã được chọn chưa)
Một số điều cần lưu ý…
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.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 sauboard_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 trellojobboard_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ụngTable
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ọ
Đặ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 create
và config
. Để 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
Để 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:
# 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:
Đố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:
# 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 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:
Để đượ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.
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>
Chúng tôi sẽ sử dụng các công cụ build
và twine
để 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:
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!
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
, add
và commit
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.
Đâ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ứ?